Files
2026-07-18 21:50:11 +05:30

77 KiB
Raw Permalink Blame History

01 · COM Fundamentals for Offensive Security

Audience / level: Red team operators; zero COM experience assumed, ramping to wire-level internals. Prerequisites: basic Windows administration (registry, services, processes), ability to read C declarations, PowerShell literacy. Estimated reading time: 90120 minutes. After this chapter you will be able to trace any COM activation from a registry lookup to the bytes on the wire, explain exactly where Windows enforces (or forgets to enforce) a security boundary, and name the mechanism every later attack chapter abuses.

Contents


1. Why COM matters to an attacker

COM (Component Object Model, introduced 1993) is Windows' binary-level component standard: a way for code in one binary — or another process, another machine, another logon session, another integrity level — to call code somewhere else through interfaces, without caring about compiler, language, or location ([12]). It is the substrate under OLE, ActiveX, DirectX, WMI, the Windows shell, Office automation, WinRT, UAC elevation, and most Microsoft management tooling. If you operate on Windows, you are already standing on COM whether you see it or not.

Beginner note: A component is just a reusable unit of code (a DLL or EXE) that other programs can load and call. COM is the contract that makes this safe across vendors: it fixes how a caller finds a component, how function calls are laid out in memory, how errors are reported, and how the component is released. Think of it as the operating system's plugin ABI — an application binary interface every language can speak.

Three properties make COM the richest execution, persistence, and escalation surface on Windows:

  1. Practically every Windows process loads the COM runtime (combase.dll / ole32.dll), and thousands of COM classes are registered by default on a clean install ([12]).
  2. Activation is driven entirely by the registry. The mapping from a 128-bit class identifier to executable code lives in HKEY_CLASSES_ROOT\CLSID, and HKEY_CLASSES_ROOT is a merged view in which per-user registrations silently override system ones (section 4.1). COM trusts that mapping; attackers edit it ([12], [13]).
  3. COM traffic is mostly in-process or local RPC, so it is nearly invisible to file-less-malware defenses: no new service, no suspicious image load path that isn't a signed Microsoft binary, no command line a SOC dashboard flags.

Key idea: Every COM attack primitive in this course — hijack, squat, relay, elevate, move laterally — reduces to one question: who controls the mapping from a GUID to code, and what identity does the resulting server run with? Keep that lens on for the whole chapter.

1.1 Core runtime components

The moving parts you will meet in every later section ([12]):

Binary / service Role
combase.dll Modern client-side COM runtime: CoCreateInstance, activation, marshaling, security. Most legacy ole32.dll exports forward here.
ole32.dll Legacy runtime (OLE1/OLE2 embedding, monikers, Running Object Table, clipboard); forwards most Co* APIs to combase.
oleaut32.dll Automation: VARIANT, BSTR, SAFEARRAY, type libraries, the universal (TypeLib-driven) marshaler, IDispatch.
rpcss.dll The COM "SCM"/activator, ROT store, OXID resolver, endpoint-mapper support. Runs inside the RpcSs service (svchost -k rpcss).
svchost -k DcomLaunch DCOM Server Process Launcher — starts out-of-process COM servers (LocalServer32 EXEs, services, dllhost.exe surrogates).
dllhost.exe /Processid:{AppID} Default surrogate process that hosts in-proc DLL classes out-of-process when an AppID carries a DllSurrogate value (empty string = default surrogate).

Beginner note: SCM here means "Service Control Manager" only by analogy — COM documentation calls its activator the SCM, but the real work is done by the RPCSS service. OXID resolver, ROT, surrogate, AppID are all defined later in this chapter; treat the table as a map you will keep coming back to.

OpSec: A defender who baselines these six components knows exactly what "normal" looks like: dllhost.exe with /Processid: arguments, svchost -k DcomLaunch spawning a LocalServer32 binary, RPCSS listening on TCP 135. Every attack chapter leaves fingerprints in this small set of processes.


2. The object model: interfaces are vtables

A COM object is reached only through an interface, and an interface is nothing more than a pointer to a vtable — an array of function pointers in a fixed order, forever. C++ abstract-base-class layout is the canonical mapping:

Client holds:   pItf ──► [ vptr ] ──► vtable[0] = QueryInterface
                                      vtable[1] = AddRef
                                      vtable[2] = Release
                                      vtable[3] = <first interface-specific method>

Beginner note: A vtable (virtual function table) is how C++ implements polymorphism: the object starts with a hidden pointer (vptr) to a table of method addresses, and calling a method means "jump to table entry N". COM simply standardizes this layout so that any compiler, any language, any process boundary produces the same memory shape. Method order never changes; an interface is immutable once published and is identified by an interface ID (IID); all methods return HRESULT; the first three slots are always IUnknown.

2.1 IUnknown: the root of every object

Every COM interface derives from IUnknown — IID {00000000-0000-0000-C000-000000000046}:

struct IUnknown {
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) = 0;
    virtual ULONG   STDMETHODCALLTYPE AddRef(void) = 0;
    virtual ULONG   STDMETHODCALLTYPE Release(void) = 0;   // object self-destructs at refcount 0
};
  • QueryInterface (QI) is the only discovery mechanism: you ask an object "do you implement IID X?" and receive either an interface pointer or E_NOINTERFACE. The contract is reflexive, symmetric, transitive, and QI(IID_IUnknown) must always return the same pointer — that pointer is the object's identity.
  • AddRef/Release implement reference counting: the object frees itself when the count reaches zero.
  • Multiple interfaces on one object = multiple vptrs (C++ multiple inheritance with adjusting thunks). IUnknown appears once per base vtable, but identity is preserved through the primary IUnknown.

Key idea: There is no type system at runtime, only QI. Anything that tricks a client into trusting the wrong vtable — a forged marshaled pointer, a swapped registry entry, a TreatAs redirect — is indistinguishable from the real object. COM's entire trust model rests on the registry and on the unmarshaling code, not on cryptography.

HRESULT is a 32-bit structured return code: bit 31 = severity (1 = failure), bits 1626 = facility (1 = RPC, 2 = Dispatch, 3 = Storage, 4 = ITF/interface-specific, 7 = Win32), bits 015 = code. Codes you will see constantly:

Constant Value Meaning
S_OK 0x00000000 Success
S_FALSE 0x00000001 Success with a boolean "false" payload
E_NOINTERFACE 0x80004002 QI failed: interface not implemented
E_POINTER 0x80004003 Invalid pointer argument
E_ACCESSDENIED 0x80070005 Launch/access permission failure (facility 7 = Win32, code 5)
REGDB_E_CLASSNOTREG 0x80040154 CLSID not found in the registry
MK_E_SYNTAX Moniker display-name parse failure
CO_E_ELEVATION_DISABLED / CO_E_MISSING_DISPLAYNAME / CO_E_RUNAS_VALUE_MUST_BE_AAA Elevation-moniker policy failures (section 10)
RPC_E_CHANGED_MODE Thread tried to change its apartment model after init

2.2 IDispatch: late binding for script (the automation engine)

IUnknown requires the caller to know the vtable layout at compile time. Scripting languages cannot do that, so automation-capable objects also implement IDispatch — IID {00020400-0000-0000-C000-000000000046} — vtable slots 36:

HRESULT GetTypeInfoCount(UINT *pctinfo);
HRESULT GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
HRESULT GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
/* [local] */ HRESULT Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
                             DISPPARAMS *pDispParams, VARIANT *pVarResult,
                             EXCEPINFO *pExcepInfo, UINT *puArgErr);
/* wire: [call_as(Invoke)] RemoteInvoke(..., UINT cVarRef, UINT *rgVarRefIdx, VARIANTARG *rgVarRef) */

Beginner note: This is late binding: instead of jumping to vtable slot N, the caller passes method names as strings. GetIDsOfNames translates names to numeric dispatch IDs (DISPIDs), and Invoke(DISPID, args) executes the method with arguments packed as an array of VARIANTs — a tagged-union type that can hold integers, strings, objects, arrays. This is exactly what New-Object -ComObject, VBScript CreateObject, JScript, VBA, and every IDispatch-only client use under the hood.

  • wFlags: DISPATCH_METHOD=0x1, DISPATCH_PROPERTYGET=0x2, DISPATCH_PROPERTYPUT=0x4, DISPATCH_PROPERTYPUTREF=0x8.
  • DISPPARAMS = { VARIANTARG *rgvarg; DISPID *rgdispidNamed; UINT cArgs; UINT cNamedArgs; } — arguments are passed in reverse order, a detail that matters when you hand-craft Invoke calls from a fuzzer or a C# reflection harness.
  • A dual interface exposes both a custom vtable and IDispatch — marked dual in IDL and described by a type library — so the same object serves compiled C++ clients and VBScript equally.

Key idea: Any class registered with the Programmable marker and a dual interface is scriptable code-execution plumbing: two lines of PowerShell, JScript, or an Office macro can instantiate it and call arbitrary methods. The classic inventory — WScript.Shell {72C24DD5-D70A-438B-8A42-98424B88AFB8}, Shell.Application {13709620-C279-11CE-A49E-444553540000}, MMC20.Application {49B2791A-B1AE-4C90-9B8E-E860BA07F889}, Office *.Application, InternetExplorer.Application {0002DF01-0000-0000-C000-000000000046}, Scripting.FileSystemObject {0D43FE01-F093-11CF-8940-00A0C9054228}, Schedule.Service {0F87369F-A4E5-4CFC-BD3E-73E6154572DD} — is the backbone of the lateral-movement and execution chapters.

2.3 Class factories and aggregation

COM never news an object directly; every creatable class exposes a class object (class factory) implementing IClassFactory — IID {00000001-0000-0000-C000-000000000046}:

HRESULT CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject); // pUnkOuter != NULL only for aggregation
HRESULT LockServer(BOOL fLock);   // pin the server in memory

IClassFactory2 {B196B28F-BAB4-101A-B69C-00AA00341D07} adds licensing (GetLicInfo, RequestLicKey, CreateInstanceLic) — relevant when a control refuses to instantiate outside a licensed container.

Aggregation vs containment. CoCreateInstance(..., pUnkOuter, ...) passes a controlling outer IUnknown; the inner object delegates its IUnknown to the outer (identity fusion) while keeping private inner vtables. Aggregation bugs — identity confusion, blind AddRef/Release through the wrong pointer — are a classic local privilege-escalation bug class known as "trapped objects", covered in the privilege-escalation chapter.


3. Identity: the GUID zoo

Everything in COM is named by a GUID: 16 bytes, string form {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} (struct fields Data1 u32, Data2/Data3 u16, Data4 u8[8]; generated by CoCreateGuid/UuidCreate, version-4 random). String↔binary conversion: CLSIDFromString/IIDFromString/StringFromGUID2, StringFromCLSID.

Beginner note: A GUID is just a very large random number used as a name. The point is collision-freedom without a central registrar: any vendor can mint IDs for their classes and interfaces. The flip side for defenders and attackers alike: a GUID alone tells you nothing — its meaning comes from the registry entries that reference it.

Name Identifies Registry home
CLSID a class (an implementation) HKCR\CLSID\{CLSID}
IID an interface (a vtable contract) HKCR\Interface\{IID}
AppID a deployment/security group of classes HKCR\AppID\{AppID}
ProgID human-readable class alias, Vendor.Component[.Version], ≤ 39 chars HKCR\<ProgID>
LIBID a type library + version (major.minor) HKCR\TypeLib\{LIBID}\<ver>
CATID a component category (e.g. "Insertable control") HKCR\Component Categories\{CATID}

Conversions: CLSIDFromProgID reads HKCR\<ProgID>\CLSID; ProgIDFromCLSID reads HKCR\CLSID\{CLSID}\ProgID; VersionIndependentProgID plus a CurVer chain resolves Vendor.ComponentVendor.Component.2 → CLSID. This is why New-Object -ComObject WScript.Shell works without anyone typing a GUID.

Key idea: Uniqueness of ProgIDs is by convention only — nothing validates collisions. Whoever writes HKCR\WScript.Shell\CLSID first (or highest in the merge order, section 4.1) decides what WScript.Shell means for that victim. ProgID squatting is therefore trivial and shows up in persistence tradecraft; version chains (CurVer) add a second, often-forgotten redirection hop to audit.


4. The registry is the COM database

COM has no central daemon holding registrations; the registry is the database, and combase.dll queries it on every activation ([12]). This section is the longest in the chapter because it is the one everything else stands on.

4.1 The merged view — the single most important security fact

HKEY_CLASSES_ROOT (HKCR) is not a real hive. It is a merged view of two underlying hives ([12], [13]):

  • HKLM\SOFTWARE\Classes — system-wide; writing requires admin/TrustedInstaller.
  • HKCU\SOFTWARE\Classes — per-user; writable by the unprivileged user.

Per-user keys win in the merged view. When a standard user writes to HKCR\..., the write is silently redirected to HKCU\Software\Classes\..., and subsequent reads through HKCR return the user's version in preference to the system's. This applies to CLSID, AppID, Interface, TypeLib, ProgIDs, and file associations alike.

Key idea: The entire CLSID→code resolution can be shadowed per-user with no privileges and no admin prompt. A standard user can redirect any CLSID their own session activates — including CLSIDs that elevated, auto-approving processes will later activate on their behalf. This is the root primitive of COM hijacking, and the persistence and UAC-bypass chapters spend their entire length exploiting this one sentence.

Figure 1 — HKCR merged view: HKLM and HKCU stacks merging into the HKCR lens, with the attacker-controlled HKCU layer and the WOW6432Node branch highlighted How to read this figure: the two source hives feed one merged view; reads prefer the HKCU layer, so any key the user plants there masks the system value below it. The WOW6432Node branch shows where 32-bit registrations live on 64-bit Windows.

64-bit vs 32-bit. On 64-bit Windows, 32-bit registrations live under HKCR\WOW6432Node\CLSID (and WOW6432Node\AppID, WOW6432Node\Interface, WOW6432Node\TypeLib); 32-bit processes writing HKLM\SOFTWARE\Classes\CLSID are redirected there. COM bitness must match the client unless an out-of-process server or the AppID value PreferredServerBitness (1 = 32-bit, 2 = 64-bit) is used; the client can force it with CLSCTX_ACTIVATE_32_BIT_SERVER=0x40000 / CLSCTX_ACTIVATE_64_BIT_SERVER=0x80000.

4.2 HKCR\CLSID\{CLSID} — anatomy of a class registration

HKCR\CLSID\{XXXXXXXX-...}
  (Default)                     = "Friendly class name"
  AppID                         = "{AppID-GUID}"            (value; links to security config)
  LocalizedString               = "@C:\path\to.dll,-101"    (value; MUI name for UAC consent)
  \InprocServer32
      (Default)                 = "C:\...\server.dll"
      ThreadingModel            = "" | "Apartment" | "Free" | "Both" | "Neutral"
  \LocalServer32
      (Default)                 = "C:\...\server.exe"  (may include args)
      ServerExecutable          = "C:\...\server.exe"  (optional)
  \InprocHandler32              = handler DLL (handler marshaling)
  \ProgID                       = "Vendor.Component.1"
  \VersionIndependentProgID     = "Vendor.Component"
  \TypeLib                      = "{LIBID}"
  \TreatAs                      = "{OtherCLSID}"   (full emulation redirect — checked FIRST)
  \AutoTreatAs                  = "{OtherCLSID}"   (auto version-update redirect)
  \Control / \Insertable / \Programmable   (marker subkeys)
  \Implemented Categories\{CATID}
  \DefaultIcon, \ToolboxBitmap32, \MiscStatus, \Version, \Verb
  \Elevation
      Enabled                   = 1 (DWORD; class is LUA/auto-elevation capable)
      IconReference             = "@path,-resid"

The read order is security-critical ([5], [6], [12], [13]):

  1. TreatAs / AutoTreatAs are checked FIRST — before any server key. If present, activation recursively restarts with the replacement CLSID and the original class's code is never touched. This is a registry-only redirect: no DLL of the real class is loaded, and the Turla "COMpfun" family used exactly this for persistence.
  2. Then the SxS activation context (section 4.7).
  3. Then InprocServer32 (bitness-aware) or LocalServer32, depending on the requested CLSCTX.

ThreadingModel semantics (why they matter in section 7): (absent) = legacy single-threaded → object must live in the first (main) STA of the process; Apartment = any STA; Free = MTA only; Both = same apartment as the client (STA or MTA); Neutral = thread-neutral apartment (COM+). A wrong model doesn't fail — COM silently spins up a different apartment and marshals the calls, which changes performance, reentrancy, and sometimes security behavior.

4.3 HKCR\AppID\{AppID} — security configuration

AppIDs group classes under one security and hosting policy. The named values below live under HKCR\AppID\{AppID}; the binary ACLs are self-relative SECURITY_DESCRIPTORs, editable in dcomcnfg.exe → Component Services → DCOM Config ([4], [11]):

Value Meaning
(Default) Friendly name
LaunchPermission Binary ACL: who may activate (checked by RPCSS at launch)
AccessPermission Binary ACL: who may call (checked in the server process at first access / by CoInitializeSecurity default)
AuthenticationLevel DWORD override of RPC_C_AUTHN_LEVEL_*
RunAs "Interactive User" or a user name → server runs as that identity (absent = launching user); cross-session activation when set to Interactive User
LocalService + ServiceParameters Run as a Windows service (started via the SCM)
DllSurrogate Custom surrogate path; empty string = default dllhost.exe
RemoteServerName Default remote host (legacy)
ActivateAtStorage DWORD 1 = activate where the persistent data lives
ROTFlags DWORD 1 = allow any client ROT visibility
AppIDFlags e.g. APPIDREGFLAGS_AAA_NO_IMPERSONATE=0x8
PreferredServerBitness 1 = 32-bit, 2 = 64-bit
LoadUserSettings Load the user hive in the server
Endpoints Restrict RPC protocol sequences (multi_sz)

Additionally HKCR\AppID\<program.exe> (named after the EXE) with value AppID={guid} maps an EXE to its AppID. Two consequences matter for offense: a class with no AppID cannot be launched remotely and cannot elevate ([14]) — DCOM lateral movement and UAC bypass both begin by finding classes that do have AppIDs — and the RunAs/DllSurrogate values decide what identity and what host process your payload inherits.

4.4 HKCR\Interface\{IID} — marshaling glue

HKCR\Interface\{IID}
  (Default)            = "IMyInterface"
  \ProxyStubClsid32    = "{CLSID of the proxy/stub DLL}"   <- loaded into BOTH client & server processes
  \ProxyStubClsid      = 16-bit legacy
  \NumMethods          = "7"
  \BaseInterface       = "{IID of parent}"
  \TypeLib             = "{LIBID}" (+ \Version)
  • ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046} (PSOAInterface, the oleaut32 "universal marshaler") means the interface is marshaled TypeLib-driven — oleaut32 reads the registered type library at runtime and builds proxies on the fly (requires [oleautomation]-compatible types). PSDispatch = {00020420-0000-0000-C000-000000000046} does the same for pure IDispatch.
  • Otherwise the CLSID points at a MIDL-generated proxy/stub DLL, which itself has a normal HKCR\CLSID entry with InprocServer32 ([38]).

Key idea: This key is the marshaling trust anchor. A per-user shadow of HKCR\Interface\{IID}\ProxyStubClsid32 injects a DLL into every process that ever marshals that interface — including privileged COM servers that call across apartments or processes. One registry value, code execution on both sides of the boundary. The privilege-escalation chapter builds a full LPE chain on it.

4.5 HKCR\TypeLib\{LIBID} — type libraries

A type library (TLB) is the compiled metadata describing interfaces, methods, and types — what the universal marshaler, IDispatch clients, and IDEs read ([39], [41]).

HKCR\TypeLib\{LIBID}
  \<major.minor>          (Default) = "MyLib 1.0 Type Library"
      \0\win32            (Default) = "C:\...\lib.tlb"     (0 = LCID-neutral)
      \0\win64            (Default) = 64-bit tlb
      \9\win32            (Default) = LCID 9 (en-US) localized tlb
      \FLAGS              = "0"  (LIBFLAG: 1=RESTRICTED, 2=CONTROL, 4=HIDDEN, 8=HASDISKIMAGE)
      \HELPDIR            = path

APIs: LoadTypeLib(path) / LoadTypeLibEx, LoadRegTypeLib(LIBID, wVerMajor, wVerMinor, lcid), RegisterTypeLib/UnRegisterTypeLib, QueryPathOfRegTypeLib. Type libraries are binary (magic MSFT = 0x5446534D) or embedded in PEs as TYPELIB resources; stdole2.tlb/stdole32.tlb are the system ones.

Warning: LoadTypeLib moniker-parsing quirk (Outflank research, [23]): if the path string is not a standalone or embedded TLB, the API falls back to parsing the string as a moniker display name. "script:http://evil/x.sct" therefore reaches the script moniker (section 8) and executes code — from what every developer assumes is a data-only API. This underpins both TypeLib hijacking (persistence chapter) and the Visual Studio poisoning attack.

4.6 Machine-wide OLE policy — HKLM\SOFTWARE\Microsoft\Ole

These values set the ceiling every AppID lives under ([27]):

Value Meaning
EnableDCOM ("Y"/"N") Master remote-activation switch
EnableRemoteConnect ("Y"/"N") Remote access (non-activation) switch
DefaultLaunchPermission / DefaultAccessPermission Binary ACLs used when an AppID omits its own
MachineLaunchRestriction / MachineAccessRestriction Hard ceiling ACLs applied to every activation/access
LegacyAuthenticationLevel (DWORD) Process default when the app never calls CoInitializeSecurity (2 = CONNECT; raised to PKT_INTEGRITY = 5 by DCOM hardening)
LegacyImpersonationLevel (DWORD) Default client impersonation level (commonly 2 = Identify; many vendor hardening scripts set 3 = Impersonate)
AppCompat\RequireIntegrityActivationAuthenticationLevel (DWORD) CVE-2021-26414 kill-switch: 1 = DCOM servers reject activation below RPC_C_AUTHN_LEVEL_PKT_INTEGRITY
AppCompat\RaiseActivationAuthenticationLevel (DWORD) 2 = clients auto-raise non-anonymous activation to PKT_INTEGRITY (Nov 2022)

Warning: The current defaults of LegacyAuthenticationLevel/LegacyImpersonationLevel on fully patched Windows 11 are version-dependent — the DCOM hardening rollout has shifted them across releases. Verify on your exact lab build before relying on any documented value (this flag is carried from the research brief's confidence notes).

4.7 Registration-free COM (SxS)

Classes can be registered in an activation-context manifest instead of the registry: <file><comClass clsid="..." threadingModel="..." tlbid="..." progid="..."/></file>, <comInterfaceExternalProxyStub iid="..." proxyStubClsid32="...">, <typelib tlbid="...">, driven by CreateActCtx/ActivateActCtx; Windows itself ships manifests under %WinDir%\WinSxS\Manifests. The lookup order during activation is: active activation context → process manifest → application manifest → registry.

Key idea: Reg-free entries are invisible to naive HKCR hunting, and an attacker-controlled manifest for a hijacked CLSID needs no registry write at all — a stealthy persistence/hijack path that defeats both detection rules keyed on HKCU\Software\Classes and many hunting playbooks. The persistence chapter weaponizes this.


5. Activation, step by step

Activation is everything that happens between "client asks for a CLSID" and "client holds a usable interface pointer". Every later chapter hijacks one of the steps below.

5.1 Client-side APIs (exact signatures)

HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);
//  dwCoInit: COINIT_MULTITHREADED=0x0 | COINIT_APARTMENTTHREADED=0x2
//            | COINIT_DISABLE_OLE1DDE=0x4 | COINIT_SPEED_OVER_MEMORY=0x8
HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID *ppv);
HRESULT CoGetClassObject(REFCLSID rclsid, DWORD dwClsContext, LPVOID pvReserved, REFIID riid, LPVOID *ppv);
HRESULT CoCreateInstanceEx(REFCLSID rclsid, IUnknown *punkOuter, DWORD dwClsCtx,
                           COSERVERINFO *pServerInfo, DWORD dwCount, MULTI_QI *pResults);
//  COSERVERINFO { DWORD dwReserved1; LPWSTR pwszName; COAUTHINFO *pAuthInfo; DWORD dwReserved2; }
//  MULTI_QI     { const IID *pIID; IUnknown *pItf; HRESULT hr; }
HRESULT CoGetObject(LPCWSTR pszName, BIND_OPTS *pBindOptions, REFIID riid, void **ppv);
HRESULT CLSIDFromProgID(LPCOLESTR lpszProgID, LPCLSID lpclsid);

([1], [2]) CoCreateInstance literally expands to:

CoGetClassObject(clsid, ctx, NULL, IID_IClassFactory, &pCF);
pCF->CreateInstance(pUnkOuter, riid, ppv);
pCF->Release();

CLSCTX (wtypes.h) selects where the server may run ([1]):

Constant Value Constant Value
INPROC_SERVER 0x1 DISABLE_AAA 0x8000
INPROC_HANDLER 0x2 ENABLE_AAA 0x10000
LOCAL_SERVER 0x4 FROM_DEFAULT_CONTEXT 0x20000
INPROC_SERVER16 0x8 ACTIVATE_32_BIT_SERVER 0x40000
REMOTE_SERVER 0x10 ACTIVATE_64_BIT_SERVER 0x80000
INPROC_HANDLER16 0x20 ENABLE_CLOAKING 0x100000
NO_CODE_DOWNLOAD 0x400 APPCONTAINER 0x400000
NO_CUSTOM_MARSHAL 0x1000 ACTIVATE_AAA_AS_IU 0x800000
ENABLE_CODE_DOWNLOAD 0x2000 PS_DLL 0x80000000
NO_FAILURE_LOG 0x4000 ALL = 0x17, SERVER = 0x15 composites

Figure 2 — COM activation overview: client APIs feeding combase resolution, then branching into in-proc, local OOP, and remote paths How to read this figure: follow a single CoCreateInstance call left to right — registry/TreatAs resolution first, then the CLSCTX branch. Each branch endpoint is a process boundary (or machine boundary) whose crossing is where security checks and attack surface concentrate.

5.2 In-process activation (CLSCTX_INPROC_SERVER)

  1. combase resolves HKCR\CLSID\{clsid}: TreatAs/AutoTreatAs first (recursive redirect), then the SxS activation context, then InprocServer32 in the bitness-appropriate view.
  2. LoadLibraryW(server.dll) — subject to DLL search order, KnownDLLs, and Safer/AppLocker policy.
  3. GetProcAddress("DllGetClassObject") → call DllGetClassObject(rclsid, riid, &pv). Every COM DLL exports DllGetClassObject, DllCanUnloadNow, and optionally DllRegisterServer/DllUnregisterServer (invoked by regsvr32.exe).
  4. The server returns its class factory; IClassFactory::CreateInstance news the object and QIs the requested IID.
  5. Unload path: CoFreeUnusedLibraries periodically calls DllCanUnloadNowS_OKFreeLibrary.

Key idea: Attack surface exists at every step: a TreatAs redirect (no DLL of the real class ever loads), a phantom InprocServer32 path pointing at a non-existent file you can plant, search-order planting, an unsigned DLL loaded into arbitrary — including elevated or PPL-adjacent — processes, and refcount/UAF bugs on the unload path. In-proc activation is a DLL-load primitive wearing a registry costume.

5.3 Local out-of-process activation (CLSCTX_LOCAL_SERVER)

Figure 3 — In-process vs out-of-process: the trust boundary between client address space and server process, with RPCSS/DcomLaunch as the broker How to read this figure: everything above the boundary line runs with the caller's address space and token; below it, a separate process with its own identity. The broker (RPCSS/DcomLaunch) is the only legitimate way across, and its gates are numbered in the text below.

  1. combase sees no (or an excluded) in-proc server → asks the activator over ALPC/LRPC: on modern Windows this is the ISystemActivator/IRemoteSCMActivator interface, GUID {000001A0-0000-0000-C000-000000000046}, implemented by rpcss.dll and mirrored in-process in combase for the local fast path ([18]).
  2. RPCSS validates, in order: HKLM\...\Ole!MachineLaunchRestriction → AppID LaunchPermission (or DefaultLaunchPermission) → identity selection:
    • default launching user (server started with the caller's token — integrity level inherited),
    • RunAs = specific user (prompted/stored credentials) or Interactive User (server in the currently logged-on session — the cross-session primitive, section 9.3),
    • LocalService = start a service via the SCM.
  3. RPCSS (via DcomLaunch) starts the process: the LocalServer32 command line with the -Embedding switch, or dllhost.exe /Processid:{AppID} for surrogates, or the service.
  4. The server process calls CoInitializeEx, creates its class factories, then CoRegisterClassObject(clsid, pCF, CLSCTX_LOCAL_SERVER, REGCLS_*, &dwCookie) — registration lands in RPCSS's class table. Flags: REGCLS_SINGLEUSE=0 (one activation then dead), MULTIPLEUSE=1, MULTI_SEPARATE=2, SUSPENDED=4 (register all, then CoResumeClassObjects() atomically — avoids a race where a client activates a half-initialized server), SURROGATE=8.
  5. RPCSS marshals the class-factory reference (an OBJREF, section 6.2) back to the client.
  6. Client unmarshals → proxy for IClassFactory → the CreateInstance call goes over ORPC → server returns the requested interface pointer, again as an OBJREF; the client builds the interface proxy/stub chain from HKCR\Interface\{IID}\ProxyStubClsid32.
  7. Lifetime: the client holds refs; the server stays alive until the last external reference plus LockServer count drop, then calls CoRevokeClassObject and exits.

OpSec: A LocalServer32 spawn is noisy in a good way for defenders: svchost -k DcomLaunch is the parent, the child carries -Embedding on its command line, and surrogates show /Processid:{AppID}. Any process tree matching these shapes with an unexpected child binary is an activation artifact worth an alert.

5.4 Remote activation (DCOM)

The client passes COSERVERINFO{pwszName="HOST"} (or an AppID RemoteServerName, or an ActivateAtStorage/moniker path). Local RPCSS forwards to the remote host on TCP 135 and speaks IRemoteSCMActivator opnums ([8], [9]):

Opnum Method
3 RemoteGetClassObject
4 RemoteGetInstanceFromFile
5 RemoteGetInstanceFromStorage
6 RemoteCreateInstance

Parameters travel as marshaled activation-property blobs: IActivationPropertiesIn/Out containing IInstantiationInfo {CLSID, CLSCTX, IID array}, ISecurityInfo {authn/authz levels}, and on return IActivationPropertiesOutPropsOutInfo { DWORD cifs; IID *piid; HRESULT *phr; MInterfacePointer **ppIntfData; }, where MInterfacePointer = { ULONG ulCntData; byte abData[]; } is a little-endian OBJREF ([18]). After activation, method calls flow over dynamic RPC ports negotiated via IOXIDResolver (section 6.3). The Windows Firewall exception group is "COM+ Network Access" / DCOM TCP 135 plus the dynamic range.

Key idea: The CoCreateInstanceEx DCOM path is exactly what lateral movement rides on — MMC20.Application, ShellWindows, ShellBrowserWindow, Excel DDE, Visio, Outlook, and the Control Panel abuse documented by Securelist ([15]). If you understand this one paragraph, the entire DCOM lateral-movement chapter is mechanics.

5.5 Moniker-based activation

CoGetObject(name, bindopts, iid, ppv) = MkParseDisplayName + BindToObject. Display-name parsing: the prefix before the first : is resolved as a ProgID → the class implementing IParseDisplayName ({0000001A-0000-0000-C000-000000000046}) builds the moniker (this is how clsid:, new:, session:, Elevation:, script:, queue:, soap: get their meaning); otherwise a file path builds a FileMoniker; ! composes monikers into a CompositeMoniker {00000309-0000-0000-C000-000000000046} which binds right-to-left (the left part becomes pmkToLeft). Section 8 catalogs every prefix and its abuse potential.


6. Marshaling and the wire

6.1 Proxy/stub architecture

When caller and object live in different apartments, processes, or machines, COM splits every call ([7], [9]):

  • Proxy (client side, IRpcProxyBuffer) packs arguments with NDR → channel (IRpcChannelBuffer over the RPC runtime) →
  • Stub (server side, IRpcStubBuffer::Invoke) unpacks, calls the real method, packs the return.
  • Factory: IPSFactoryBuffer (GetProxyIID, CreateProxy, CreateStub), registered via HKCR\Interface\{IID}\ProxyStubClsid32.

Beginner note: Marshaling = serializing a method call (arguments, interface pointers) so it can cross a boundary, then reconstructing it on the far side. NDR (Network Data Representation) is the DCE/RPC serialization format COM uses. You already know the trust anchor problem: the proxy/stub DLL is chosen by a registry key, and it loads into both endpoints.

Three marshaling flavors, recorded in the OBJREF flags:

  1. Standard (0x1): a MIDL-generated proxy/stub DLL, or the universal marshaler (PSOAInterface {00020424-0000-0000-C000-000000000046}) which marshals any [oleautomation] interface by reading its type library at runtime.
  2. Handler (0x2): a light client-side handler DLL (InprocHandler32) plus a standard reference.
  3. Custom (0x4): the object implements IMarshal ({00000003-0000-0000-C000-000000000046}: GetUnmarshalClass, GetMarshalSizeMax, MarshalInterface, UnmarshalInterface, ReleaseMarshalData, DisconnectObject). An OBJREF_CUSTOM carries {CLSID of unmarshal class, cbExtension, size, pData[]} — an attacker-controlled blob deserialized by IMarshal::UnmarshalInterface of an arbitrary registered class.

Key idea: Custom marshaling means "pick any registered class to parse bytes you control, inside the victim process". This is the Forged-OLEData / relay bug class — the POTATO family's CoGetInstanceFromIStorage OBJREF forgery lives here — and the mitigations (EOAC_NO_CUSTOM_MARSHAL, CLSCTX_NO_CUSTOM_MARSHAL) exist precisely because of it.

Client-side convenience APIs you will meet in exploit code: CoMarshalInterface, CoUnmarshalInterface, CoMarshalInterThreadInterfaceInStream + CoGetInterfaceAndReleaseStream, CoCreateFreeThreadedMarshaler (FTM CLSID {00000333-0000-0000-C000-000000000046} — a custom marshaler whose blob is just the raw pointer; it neutralizes apartment checks and is a classic source of "marshaled" objects being callable from any thread), and the Global Interface Table — a service-wide cookie↔pointer store: IGlobalInterfaceTable {00000146-0000-0000-C000-000000000046} on CLSID_StdGlobalInterfaceTable {00000323-0000-0000-C000-000000000046} (RegisterInterfaceInGlobal / GetInterfaceFromGlobal / RevokeInterfaceFromGlobal).

6.2 OBJREF — the marshaled interface pointer (on the wire: MEOW)

typedef struct tagOBJREF {
    unsigned long signature;   // 0x574F454D == 'MEOW' (little-endian)
    unsigned long flags;       // OBJREF_STANDARD=0x1 | HANDLER=0x2 | CUSTOM=0x4 | EXTENDED=0x8
    GUID          iid;         // IID being marshaled
    union {
        struct { STDOBJREF std; DUALSTRINGARRAY saResAddr; }                    u_standard;
        struct { STDOBJREF std; CLSID clsid; DUALSTRINGARRAY saResAddr; }       u_handler;
        struct { CLSID clsid; unsigned long cbExtension, size; byte *pData; }   u_custom;
        struct { STDOBJREF std; unsigned long Signature1; DUALSTRINGARRAY saResAddr;
                 unsigned long nElms; unsigned long Signature2; DATAELEMENT ElmArray; } u_extended;
    } u_objref;
} OBJREF;

typedef unsigned __int64 OXID;   // object exporter  (per apartment+protocol, machine-unique)
typedef unsigned __int64 OID;    // object identity  (per exporter)
typedef GUID             IPID;   // interface pointer (per object)

typedef struct tagSTDOBJREF {
    unsigned long flags;        // SORF_* (SORF_NOPING=0x1000)
    unsigned long cPublicRefs;  // refcounts transferred with this marshaling
    OXID oxid; OID oid; IPID ipid;
} STDOBJREF;

typedef struct tagSTRINGBINDING   { unsigned short wTowerId;       /* 0x07=ncacn_ip_tcp, 0x1F=ncacn_http */
                                    unsigned short aNetworkAddr; } STRINGBINDING;   // zero-terminated
typedef struct tagSECURITYBINDING { unsigned short wAuthnSvc;      /* 0x0A=NTLM(WinNT), 0x09=Kerberos, 0xFFFF=none */
                                    unsigned short wAuthzSvc; unsigned short aPrincName; } SECURITYBINDING;
typedef struct tagDUALSTRINGARRAY { unsigned short wNumEntries; unsigned short wSecurityOffset;
                                    unsigned short aStringArray[]; } DUALSTRINGARRAY;

([7], [10]) MEOW is trivially greppable in packet captures and memory — the universal indicator of a marshaled COM reference, useful both offensively (carving OBJREFs out of MInterfacePointer blobs) and for detection rules.

Figure 4 — Proxy/stub call stack and the OBJREF byte layout with the MEOW signature How to read this figure: the left half is the call path (client → proxy → channel → stub → object); the right half is what an interface pointer becomes on the wire — signature, flags, IID, then the STDOBJREF carrying OXID/OID/IPID plus the string bindings that tell the client where the server listens.

6.3 ORPC call flow, OXID resolution, liveness

  1. The client unmarshals an OBJREF and needs the exporter's RPC bindings: it calls IOXIDResolver (a.k.a. IObjectExporter, UUID {99FCFEC4-5260-101B-BBCB-00AA0021347A}, reached on the exporter machine via RPCSS/135) → ResolveOxid2(oxid, cRequestedProtseqs, arRequestedProtseqs[], &dsa, &ipidRemUnknown, &authnHint, &comVersion) returns the string bindings — i.e. the dynamic TCP port ([7], [9]).
  2. Calls are then ordinary MS-RPCE with an ORPC extension (ORPCTHIS/ORPCTHAT: causality GUID, extensions) to the resolved endpoint, dispatched by IPID → stub manager → interface stub → apartment → method. Standard IPIDs: IRemUnknown {00000131-0000-0000-C000-000000000046} (RemQueryInterface, RemAddRef, RemRelease) and IRemUnknown2 {00000143-...} handle remote QI and refcounting.
  3. Pinging/liveness: IOXIDResolver::SimplePing(pingId) and ComplexPing keep OIDs alive; missed pings ⇒ the server garbage-collects stubs; a client Release becomes RemRelease, decrementing cPublicRefs.

Figure 5 — DCOM on the wire: TCP 135 activation, ResolveOxid2, dynamic-port method calls, and ping traffic How to read this figure: two conversations are interleaved — the activation exchange against port 135 (IRemoteSCMActivator) and the per-object conversation against a dynamic port learned via ResolveOxid2. When you sniff a DCOM lateral movement, expect exactly this two-stage pattern.


7. Apartments and threading

An apartment is COM's unit of thread-affinity: every thread that touches COM belongs to exactly one, and every object lives in exactly one.

  • A thread joins COM via CoInitializeEx(NULL, COINIT_APARTMENTTHREADED=0x2)STA (single-threaded apartment; one per thread). COM creates a hidden window — class OleMainThreadWndClass — whose WndProc dispatches cross-apartment calls ([30]).
  • COINIT_MULTITHREADED=0x0MTA (multi-threaded apartment; one per process). A thread that never initializes COM lands in the implicit MTA once the process uses COM. Re-initializing with a conflicting model returns RPC_E_CHANGED_MODE.
  • ThreadingModel=Neutral objects live in the TNA (thread-neutral apartment, COM+) and can be called from any apartment without a proxy.

Rules that drive real bugs ([29], [30], [41]):

  1. STA owners must pump messages (GetMessage/DispatchMessage). Otherwise cross-apartment calls into its objects hang, and the reverse-channel reentrancy that occurs while the STA is outbound-calling something else is a classic deadlock — and a classic exploit-reliability problem, because your trigger and your payload race on the message loop.
  2. Incoming ORPC calls to an STA are posted as window messages; to an MTA they execute directly on RPC thread-pool threads, so MTA objects must self-synchronize.
  3. Passing an interface pointer across apartments requires marshaling (CoMarshalInterThreadInterfaceInStream, or the GIT) — except for objects that aggregate the FTM (section 6.1), which bypass apartment checks entirely, or Neutral components.

Key idea: Wrong ThreadingModel never fails loudly — COM silently marshals. That silent behavior change is why exploits that work in a console PoC die inside a real target: the target's apartment decided your object now lives behind a message pump you don't control. When weaponizing STA-based triggers (Office, shell, IE), build the pump into your harness or move the work to an MTA thread.


8. Monikers and the Running Object Table

A moniker is a persistent, composable "name of an object": an object that knows how to bind itself — turn a name back into a live interface pointer. IMoniker ({0000000F-0000-0000-C000-000000000046}) methods: BindToObject, BindToStorage, Reduce, ComposeWith, Enum, IsEqual, Hash, IsRunning, GetTimeOfLastChange, Inverse, CommonPrefixWith, RelativePathTo, GetDisplayName, ParseDisplayName, IsSystemMoniker ([11]).

The bind context IBindCtx ({0000000E-...}) carries BIND_OPTS{cbStruct, grfFlags, grfMode, dwTickCountDeadline}; BIND_OPTS2 adds {dwTrackFlags, dwClassContext, locale, pServerInfo}; BIND_OPTS3 adds hwnd — a consent-UI parent window used with elevation monikers (section 10).

8.1 System moniker catalog

Moniker CLSID Prefix Notes
File {00000303-0000-0000-C000-000000000046} path Binds via the class of the file extension
Item {00000304-...} !item Composite right part
Anti {00000305-...} Inverse/cancel
Pointer {00000306-...} Wraps a live pointer
Composite {00000309-...} A!B Left-to-right composition, right-to-left bind
Class {0000031A-...} clsid:{...} Thin wrapper over CoGetClassObject/CreateInstance
New {ECABAFC6-7F19-11D2-978E-0000F8757E2A} new:{clsid} or new:ProgID Instantiate via moniker (Office-blacklisted)
Session (CLSID unverified — confirm in OleViewDotNet; syntax documented) session:N!new:{clsid} Activates in another logon session (cross-session LPE primitive; cf. Forshaw research / CVE-2017-0100 class)
Elevation (implemented in combase; CLSID unverified) Elevation:Administrator!new:{clsid} / Elevation:Highest!new: UAC bridge (section 10)
Script {06290BD3-48AA-11D2-8432-006008C3FBFC} script:URL / scriptlet:URL scrobj.dll; downloads and runs a Windows Script Component; ProgIDs script, scriptlet ([24])
Scriptlet Factory {06290BD2-...} (Scriptlet.Factory) IPersistMoniker-bindable scriptlet constructor ([40])
Queue {ECABAFC7-7F19-11D2-978E-0000F8757E2A} queue: MSMQ queued components
SOAP {ECABB0C7-7F19-11D2-978E-0000F8757E2A} soap:wsdl=... CVE-2017-8759 (.NET WSDL RCE)

Warning (carried from the research brief's confidence notes): the Session and Elevation moniker implementation CLSIDs are unverified — the display-name syntax above is documented, but confirm the numeric CLSIDs in OleViewDotNet before citing them in reporting.

Key idea: Any API that accepts a string and internally binds it — CoGetObject, MkParseDisplayName, OLE object embedding, LoadTypeLib (section 4.5) — is a moniker injection point. The dangerous prefixes (script:, new:, session:, Elevation:, composites with !) turn "a path parameter" into arbitrary class instantiation, script download, cross-session activation, or a UAC bridge. This is why the execution chapter treats every string-accepting COM API as suspect.

8.2 The Running Object Table (ROT)

The ROT is a per-session table, hosted in RPCSS, of objects that have declared themselves "running". Clients get it via GetRunningObjectTableIRunningObjectTable ({00000010-0000-0000-C000-000000000046}); servers register with Register(grfFlags, punk, pmkName, &dwCookie) using ROTFLAGS_REGISTRATIONKEEPSALIVE=0x1 and ROTFLAGS_ALLOWANYCLIENT=0x2; lookup via GetObject, IsRunning, EnumRunning; GetActiveObject(clsid) consults the ROT.

Abuse primitives, both exercised in later chapters:

  • ROT squatting — register an attacker object under a well-known display name before the legitimate server does; clients that ask the ROT get you instead.
  • ROT snooping/harvestingEnumRunning reveals other applications' live objects; Office documents expose their full object models through the ROT, a goldmine for data theft and macro-less execution.
  • Cross-integrity exposureROTFLAGS_ALLOWANYCLIENT plus AppID ROTFlags=1 makes a server's objects visible to clients at other integrity levels.

9. The COM security model

9.1 CoInitializeSecurity — the process-wide baseline

Callable once per process (if never called, the first proxy operation auto-invokes it with registry defaults) ([2], [28]):

HRESULT CoInitializeSecurity(
  PSECURITY_DESCRIPTOR         pSecDesc,      // access SD (or AppID GUID with EOAC_APPID, or IAccessControl*)
  LONG                         cAuthSvc,      // -1 = default list
  SOLE_AUTHENTICATION_SERVICE *asAuthSvc,
  void                        *pReserved1,
  DWORD                        dwAuthnLevel,  // RPC_C_AUTHN_LEVEL_*
  DWORD                        dwImpLevel,    // RPC_C_IMP_LEVEL_*
  void                        *pAuthList,     // SOLE_AUTHENTICATION_LIST
  DWORD                        dwCapabilities,// EOAC_*
  void                        *pReserved3);

RPC_C_AUTHN_LEVEL_*: DEFAULT=0, NONE=1, CONNECT=2, CALL=3, PKT=4, PKT_INTEGRITY=5, PKT_PRIVACY=6. RPC_C_IMP_LEVEL_*: DEFAULT=0, ANONYMOUS=1, IDENTIFY=2, IMPERSONATE=3, DELEGATE=4. EOAC_*: MUTUAL_AUTH=0x1, SECURE_REFS=0x2, ACCESS_CONTROL=0x4, APPID=0x8, DYNAMIC=0x10, STATIC_CLOAKING=0x20, DYNAMIC_CLOAKING=0x40, ANY_AUTHORITY=0x80, MAKE_FULLSIC=0x100, REQUIRE_FULLSIC=0x200, AUTO_IMPERSONATE=0x400, DEFAULT=0x800, DISABLE_AAA=0x1000, NO_CUSTOM_MARSHAL=0x2000.

Beginner note: Authentication level answers "how sure are we who is calling, and how protected are the packets?" (NONE = not at all; PKT_INTEGRITY = signed; PKT_PRIVACY = encrypted). Impersonation level is set by the client and answers "what may the server do with my identity?": IDENTIFY lets the server check who you are, IMPERSONATE lets it act as you locally, DELEGATE lets it act as you on other machines.

9.2 Where checks happen — the defense-in-depth chain

  1. Launch (RPCSS): MachineLaunchRestriction → AppID LaunchPermission (else DefaultLaunchPermission). Failure ⇒ E_ACCESSDENIED at activation and the classic "DistributedCOM 10016" log noise (event IDs 10015/10016 family).
  2. Access (first call into the server): the server's CoInitializeSecurity SD, else AppID AccessPermission, else DefaultAccessPermission, all gated by MachineAccessRestriction.
  3. Authentication-level negotiation: client vs server vs machine legacy defaults; since the CVE-2021-26414 hardening, non-anonymous activation is floored at PKT_INTEGRITY (section 9.5).
  4. Impersonation (server side): CoImpersonateClient()/CoRevertToSelf() — the client-supplied dwImpLevel caps what the server may do (Anonymous < Identify < Impersonate < Delegate). Cloaking (EOAC_STATIC/DYNAMIC_CLOAKING) decides which token the proxy actually presents.

Key idea: The impersonation contract is where the potato family lives. If a high-privilege server calls back into you with IMPERSONATE or DELEGATE, you hold its token; cloaking bugs and wrong-identity activation are how "token kidnap" happens. Chain gates 13 decide whether you may talk; gate 4 decides whose token you walk away with.

Figure 6 — The impersonation contract: client-set dwImpLevel capping what the server may do with the client token How to read this figure: the arrow from client to server carries both the call and a token-usage grant. The four levels are a ladder; a server that only checks "did you authenticate?" but ignores the granted level is exactly the bug class the potato exploits abuse.

9.3 Server identity selection

Chosen at launch from the AppID (section 4.3): launching user (default; the activator's token — integrity level is inherited: a medium-IL activator gets a medium-IL server), a RunAs user, LocalService, or Interactive User — which creates the object in whatever session is currently interactive, enabling cross-session activation by lower-privileged users when Launch/Access ACLs allow it.

Real-world anchor: bohops' CVE-2023-33127 chain activated the PhoneExperienceHost CLSID {7540C300-BE9B-4C0D-A335-F002F9AB73B7} cross-session ([16]). The general discovery method is an OleViewDotNet filter for "Run as: Interactive User" (section 13).

9.4 Cloaking

Without cloaking, a server calling out presents its process token; with EOAC_STATIC_CLOAKING it presents a token captured at first call, with EOAC_DYNAMIC_CLOAKING the thread's current token. Misunderstanding which token flows is the root of "wrong-identity" bugs during activation — the privilege-escalation chapter details how activation-time cloaking mistakes yield SYSTEM tokens.

9.5 DCOM hardening (CVE-2021-26414, KB5004442)

Windows DCOM Server Security Feature Bypass: Microsoft phased in a minimum of RPC_C_AUTHN_LEVEL_PKT_INTEGRITY for non-anonymous activation ([20], [21], [22]):

Date Change
Jun 2021 Opt-in: HKLM\SOFTWARE\Microsoft\Ole\AppCompat!RequireIntegrityActivationAuthenticationLevel=1
Jun 2022 Default-on
Nov 2022 Clients auto-raise: RaiseActivationAuthenticationLevel=2
Mar 2023 Mandatory

Telemetry: System log, source DistributedCOM, events 10036 (server rejected activation below the floor), 10037/10038 (client below the floor) — also prime detection content for spotting lateral-movement activation attempts in the wild.


10. The elevation bridge

The elevation moniker is COM's built-in UAC bridge: a display string that asks for an object to be created elevated ([3]):

CoGetObject("Elevation:Administrator!new:{CLSID}", &bind_opts3_with_hwnd, riid, &ppv)

The call routes through the AppInfo service (appinfo.dll, RAiLaunchAdminProcess), which evaluates the class's elevation policy and shows consent UI — or auto-approves. Class requirements, verified verbatim against Microsoft's "The COM Elevation Moniker" documentation and independent requirement testing ([3], [31]):

  • Registered under HKLM only — HKCU registrations cannot elevate (a CO_E_ELEVATION_DISABLED path guard);
  • HKCR\CLSID\{CLSID}\Elevation\Enabled = 1 (else CO_E_ELEVATION_DISABLED);
  • HKCR\CLSID\{CLSID}\LocalizedString = @dll,-resid — an MUI display name (else CO_E_MISSING_DISPLAYNAME; MUI load errors from RegLoadMUIStringW propagate);
  • Optional Elevation\IconReference; the binary must be signed for the icon to display;
  • The class must run as launching user / Activate-as-Activator, else CO_E_RUNAS_VALUE_MUST_BE_AAA;
  • An already-elevated client gets no UI.
  • BIND_OPTS3's hwnd parents the consent dialog to a window.

Two elevation classes exist: prompted (consent UI — legitimate) and auto-elevated (silent for auto-approved binaries). The auto-elevated class inventory is what the CMSTPLUA bypass family abuses: {3E5FC7F9-9A51-4367-9063-A120244FBEC7} + ICMLuaUtil {6EDD6D74-C007-4E75-B76A-E5740995E24C}ShellExec ([32], [33]); in-the-wild use is documented in the ALPHV ransomware and loader campaigns ([34], [35], [37]). The server-side spawn artifact is an elevated dllhost.exe /Processid:{3E5FC7F9-...} or the LocalServer32 EXE at High IL ([34], [36]).

Figure 7 — The elevation bridge: elevation moniker string → AppInfo policy evaluation → consent or auto-approval → High-IL COM server How to read this figure: the same string either dies at a policy gate (missing HKLM registration, missing Elevation\Enabled, missing LocalizedString, wrong RunAs) or emerges on the right as a High-integrity server. Auto-approved classes skip the consent box entirely — that gap is the UAC-bypass chapter.

Key idea: The elevation bridge is a policy engine keyed on registry values — and section 4.1 told you who can write registry values. The UAC chapter is devoted to the gap between "must be under HKLM" and everything attackers do to get a process that already runs elevated to look somewhere else. This section exists so you can read that chapter's requirements checks as a checklist rather than magic.


11. WinRT and .NET bridges

WinRT is COM vNext, not a replacement ([17], [42]):

  • Same IUnknown base; IInspectable (IID {AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90}; GetIids, GetRuntimeClassName, GetTrustLevel) replaces IDispatch.
  • Metadata lives in .winmd files (ECMA-335 format) instead of TLBs.
  • Activation via RoGetActivationFactory(HSTRING clsid, IID, void**) / RoActivateInstance.
  • Classes register per-package under HKLM\SOFTWARE\Microsoft\WindowsRuntime (with ActivatableClassId server mappings) — not HKCR\CLSID — so classic hijack tooling doesn't see them. Language projections (C++/WinRT, C#/WinRT, RoGetActivationFactory shims) sit on top. Forshaw's OleViewDotNet parses both worlds.

.NET interop: managed classes exposed to COM are hosted by mscoree.dll as the InprocServer32, with values Class, Assembly, RuntimeVersion, CodeBase. Managed clients get an RCW (runtime callable wrapper, System.Runtime.InteropServices); COM clients of managed servers get a CCW (COM callable wrapper). Type libraries come from regasm /tlb / tlbexp; [ComImport]+[Guid] declares native interfaces in managed code.

Key idea: OleViewDotNet builds dynamic IDispatch stubs at runtime to call arbitrary vtables from C# — the same trick the incendium.rocks fuzzing methodology uses to automate Invoke against every registered class ([26]). If you can describe an interface, you can call it from PowerShell or C#; no C++ required. The Research Lab (section 14) turns this into a workbench.


12. The attack-surface map

Every row below is a primitive you can now explain from first principles; the "mechanism" column points back to the section of this chapter that defines it.

# Primitive (rooted in this chapter) Mechanism Attack family
1 HKCU shadowing of CLSID\{X}\InprocServer32/LocalServer32 Merged-view precedence (4.1) → per-user persistence/exec; into auto-elevated procs → UAC bypass Persistence / UAC
2 TreatAs/AutoTreatAs redirect 4.2 — checked before the server key (used by Turla "COMpfun") Persistence
3 Phantom InprocServer32/LocalServer32 paths 5.2 — load of a non-existent file → plant binary Persistence
4 Interface\{IID}\ProxyStubClsid32 shadow 4.4 → DLL into both endpoints, incl. SYSTEM servers LPE / Persistence
5 TypeLib\{LIBID}\...\win32 path shadow + LoadTypeLib moniker quirk 4.5 Persistence / Execution
6 ProgID/VersionIndependentProgID/CurVer squatting 3, 4.1 Persistence
7 Moniker string injection (script:, new:, session:, Elevation:, composite) 5.5, 8 — CoGetObject/moniker-accepting APIs (Office OLE, LoadTypeLib, SCT/scriptlet) Execution
8 ROT squatting/snooping 8.2 LPE / Execution
9 Cross-session activation (RunAs=Interactive User) 9.3 LPE
10 DCOM remote activation of LM-friendly classes 5.4 Lateral movement
11 Custom-marshal blob abuse (OBJREF_CUSTOM forgery, IMarshal parsing bugs) 6.1/6.2 Execution / Relay
12 Surrogate abuse (DllSurrogate → attacker DLL in dllhost) 4.3 Persistence / Execution
13 Reg-free COM manifest hijack 4.7 Persistence / Execution

Figure 8 — COM architecture attack-surface map: registry, activation, marshaling, and security layers with hijack points How to read this figure: each layer of the COM stack is annotated with the hijack primitive that targets it. Use it as a mental index — given any attack name from later chapters, locate its layer and you can predict its prerequisites and telemetry.

Representative CVEs anchored in fundamentals. CVE-2015-2370: DCOM activation RPC replay → SYSTEM LPE — an NTLM-authenticated ResolveOxid2 followed by a replayed RemoteCreateInstance ([19]). CVE-2017-8759: the soap: WSDL moniker driving .NET code generation → RCE, exploited in the wild ([25]). CVE-2018-0824: "Microsoft COM for Windows RCE" — ole32 deserialization of crafted OLE/moniker data delivered via Office OLE2 objects, exploited in the wild and patched May 2018; describe it as a moniker/OLE deserialization RCE. CVE-2017-0100: Windows COM/DCOM elevation-of-privilege involving HelpPane auto-elevation and activation identity (Forshaw) — moderate confidence on the exact mechanism attribution; verify before citing (flag carried from the research brief). CVE-2021-26414: the DCOM activation authentication-level bypass behind the hardening saga of section 9.5 ([20], [21]). Office responded to the moniker wave with a per-application kill list at HKLM\SOFTWARE\Microsoft\Office\<ver>\Common\COM Compatibility\{CLSID} ("ActivationFilter"/kill bits) covering the htafile {3050F4D8-...}, scriptlet {06290BD3-...}, Composite {00000309-...}, File {00000303-...}, New {ECABAFC6-...}, and SOAP {ECABB0C7-...} monikers ([25]).


13. Tooling and lab setup

Tool What you use it for
OleViewDotNet ([17]) The standard recon tool: enumerates classes/interfaces/AppIDs/typelibs (including per-user and WOW64 views), shows AppID ACLs/RunAs/Elevation flags, parses vtables from typelibs and .winmd, ROT viewer, invokes arbitrary methods, spawns elevated objects (CreateElevated), filter "Run as Interactive User".
oleview.exe (SDK) Legacy Microsoft viewer; quick sanity checks of HKCR trees.
dcomcnfg.exe GUI editor for AppID Launch/Access ACLs and machine OLE policy (section 4.6).
regedit / reg query HKCR\CLSID /s / PowerShell Get-ChildItem Registry::HKEY_CLASSES_ROOT\CLSID Raw registry ground truth.
ProcMon Filter Path contains InprocServer32/LocalServer32/TreatAs/ProxyStubClsid32, Result = NAME NOT FOUND for phantom-path hunting.
Wireshark dcom/dcerpc dissectors; grep MEOW (0x574F454D) to spot marshaled references ([7], [9]).
RpcView Maps RPC interfaces/IPIDs to processes — who exports which COM objects.
Event Viewer System log, source DistributedCOM: 10015/10016 (launch/access denials), 1003610038 (hardening floor).
API Monitor / WinDbg bp combase!CoCreateInstance — watch every activation with its CLSID and CLSCTX.

Lab layout (from the brief's lab notes): two domain-joined VMs for DCOM lateral-movement work; one VM with a standard (non-admin) user for HKCU-hijack labs; snapshot before every regsvr32/manifest experiment; enable the "Microsoft-Windows-COMRuntime" ETW provider plus OLE32/COM ETW providers for activation tracing.


14. Research Lab: Building Your COM Research Workbench

This chapter's lab is not a bug hunt; it is the workbench every later bug hunt depends on. Build it once, snapshot it, and reuse it for the rest of the course.

14.1 Hunting hypothesis

Fundamentals-level bugs still hide where the documented model and the implementation disagree: redirection hops (TreatAs, CurVer, SxS contexts) applied in an order nobody audited; string-parsing fallbacks like LoadTypeLib's moniker path ([23]); per-user shadows of keys defenders assume are HKLM-only (ProxyStubClsid32, TypeLib paths); and default ACL/authn values that shift between builds (section 4.6 warning). Any place a registry value chooses code is a candidate.

14.2 Enumeration — build the target list

PowerShell sweeps against the merged view (illustrative — run as both admin and standard user to see both layers):

# All classes with an Elevation subkey (auto-elevation candidates, section 10)
Get-ChildItem Registry::HKEY_CLASSES_ROOT\CLSID -ErrorAction SilentlyContinue |
  Where-Object { Test-Path "$($_.PSPath)\Elevation" } |
  ForEach-Object { "{0}  {1}" -f $_.PSChildName, (Get-ItemProperty $_.PSPath).'(default)' }

# Per-user shadows: CLSIDs present in BOTH HKCU and HKLM (section 4.1)
$hkcu = Get-ChildItem Registry::HKEY_CURRENT_USER\Software\Classes\CLSID -ErrorAction SilentlyContinue
$hklm = Get-ChildItem Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID -ErrorAction SilentlyContinue
Compare-Object ($hkcu  | ForEach-Object PSChildName) ($hklm | ForEach-Object PSChildName) -IncludeEqual -ExcludeDifferent

# Phantom InprocServer32/LocalServer32 targets: registered path does not exist on disk (section 5.2)
Get-ChildItem Registry::HKEY_CLASSES_ROOT\CLSID -ErrorAction SilentlyContinue | ForEach-Object {
  foreach ($sub in 'InprocServer32','LocalServer32') {
    $p = "$($_.PSPath)\$sub"
    if (Test-Path $p) {
      $path = (Get-ItemProperty $p).'(default)'
      if ($path -and -not (Test-Path ($path -replace '"','' -replace ' .*$','') -ErrorAction SilentlyContinue)) {
        "{0} [{1}] -> {2}" -f $_.PSChildName, $sub, $path }
    }
  }
}

# Surrogates and cross-session servers (sections 4.3, 9.3)
Get-ChildItem Registry::HKEY_CLASSES_ROOT\AppID -ErrorAction SilentlyContinue | ForEach-Object {
  $v = Get-ItemProperty $_.PSPath
  if ($v.DllSurrogate -ne $null -or $v.RunAs -eq 'Interactive User') {
    "{0}  RunAs={1}  DllSurrogate='{2}'" -f $_.PSChildName, $v.RunAs, $v.DllSurrogate }
}

Cross-check in OleViewDotNet: per-user view, WOW64 view, "Run as Interactive User" filter, and the ROT viewer ([17]).

14.3 Triage heuristics

A registration is a promising research/hijack target if:

  • It lives in HKLM but is shadowable from HKCU (most are, section 4.1);
  • Its server binary is unsigned, on a writable path, or missing (phantom);
  • It has an AppID with RunAs=Interactive User, empty DllSurrogate, or weak Launch/Access ACLs;
  • It exposes a dual interface with powerful methods (file, process, registry, network verbs);
  • It is marked Programmable (script-reachable) or implements IParseDisplayName/IMarshal (string/blob parsing).

14.4 Analysis workflow

  1. Baseline first. Snapshot the VM; export HKCU\Software\Classes and HKLM\SOFTWARE\Classes; record running dllhost instances and DistributedCOM event baselines. Every experiment gets a before/after diff.
  2. Static: read the registration (section 4.2 checklist), the AppID, the interface's ProxyStubClsid32, the typelib; confirm threading model and elevation flags in OleViewDotNet.
  3. Dynamic: ProcMon with the section-13 filters during activation; WinDbg bp combase!CoCreateInstance and bp combase!CoGetObject to capture exact strings and CLSCTX; Wireshark for any remote path (grep MEOW).
  4. ETW tracing: enable the "Microsoft-Windows-COMRuntime" provider plus OLE32/COM ETW providers before the trigger, then correlate activation events with process spawns (provider names per the brief; collection via your usual ETW tooling — illustrative).
  5. Patch-diffing habit: when a COM CVE drops, diff combase.dll/ole32.dll/rpcss.dll between builds and map changed functions back to this chapter's mechanisms (activation gates, OBJREF parsing, elevation policy). The CVE-2021-26414 saga (section 9.5) shows how much telemetry and behavior a single floor value changes ([20], [21]).

14.5 Fuzzing/mutation ideas

Follow the incendium.rocks methodology ([26]): enumerate registry/CLSIDs → dump each class's interfaces and methods to JSON (OleViewDotNet-style typelib parsing) → build dynamic IDispatch stubs → automated Invoke with mutated arguments. Fundamentals-specific mutations: typed VARIANT confusion in DISPPARAMS (remember reverse argument order), over-long/embedded-null strings for any name-accepting method, and DISPID values outside the typelib's declared range.

14.6 From crash/lead to primitive

A crash inside a COM server is only interesting once you control the input channel: prove reachability by driving it through the same activation path a real attacker would use (HKCU shadow → in-proc load, or DCOM activation → ORPC call), then check the server's identity (section 9.3) — a crash in a RunAs=Interactive User or SYSTEM service is an LPE lead; in a per-user server, persistence or execution. Document the registry prerequisites next; they are usually the real bug.

14.7 Further reading

  • Automating COM/DCOM vulnerability research — incendium.rocks ([26])
  • COM in Sixty Seconds — James Forshaw ([42])
  • Attacking Visual Studio for Initial Access — Outflank ([23])
  • COM Hijacking in Windows — cyberc0re ([13])
  • The Component Object Model — magicsplat ([12])
  • OleViewDotNet — James Forshaw ([17])

15. Quick-reference GUID appendix

Item GUID
IID_IUnknown 00000000-0000-0000-C000-000000000046
IID_IClassFactory 00000001-0000-0000-C000-000000000046
IID_IMarshal 00000003-0000-0000-C000-000000000046
IID_IMoniker / IBindCtx / IROT 0000000F-… / 0000000E-… / 00000010-… (-C000-000000000046)
IID_IParseDisplayName 0000001A-0000-0000-C000-000000000046
IID_IDispatch 00020400-0000-0000-C000-000000000046
IID_ITypeInfo / ITypeLib 00020401-… / 00020402-…
PSDispatch / PSOAInterface (universal marshaler) 00020420-… / 00020424-0000-0000-C000-000000000046
IID_IRemUnknown / IRemUnknown2 00000131-… / 00000143-…
IGlobalInterfaceTable; CLSID_StdGIT 00000146-…; 00000323-0000-0000-C000-000000000046
CLSID_FreeThreadedMarshaler 00000333-0000-0000-C000-000000000046
IRemoteSCMActivator / ISystemActivator 000001A0-0000-0000-C000-000000000046
IOXIDResolver (IObjectExporter) 99FCFEC4-5260-101B-BBCB-00AA0021347A
Monikers: File/Item/Anti/Pointer/Composite/Class 00000303/…304/…305/…306/…309/…31A -C000-000000000046
NewMoniker ECABAFC6-7F19-11D2-978E-0000F8757E2A
ScriptMoniker (scrobj.dll) 06290BD3-48AA-11D2-8432-006008C3FBFC
QueueMoniker / SoapMoniker ECABAFC7-… / ECABB0C7-7F19-11D2-978E-0000F8757E2A
CMSTPLUA / ICMLuaUtil 3E5FC7F9-9A51-4367-9063-A120244FBEC7 / 6EDD6D74-C007-4E75-B76A-E5740995E24C
CLSID_FileOperation (IFileOperation host) 3AD05575-8857-4850-9277-11B85BDB8E09
Task Scheduler (Schedule.Service) 0F87369F-A4E5-4CFC-BD3E-73E6154572DD
WScript.Shell / Shell.Application / IE 72C24DD5-D70A-438B-8A42-98424B88AFB8 / 13709620-C279-11CE-A49E-444553540000 / 0002DF01-0000-0000-C000-000000000046
IInspectable (WinRT) AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90

16. References

[1] CoCreateInstance function; CLSCTX enumeration — Microsoft Learn — https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance ; https://learn.microsoft.com/en-us/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx [2] CoInitializeEx function; CoInitializeSecurity function — Microsoft Learn — https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex ; https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializesecurity [3] The COM Elevation Moniker — MicrosoftDocs/win32 (registry requirements LocalizedString / Elevation\Enabled / error codes) — https://github.com/MicrosoftDocs/win32/blob/docs/desktop-src/com/the-com-elevation-moniker.md [4] AppID Key — Microsoft Learn — https://learn.microsoft.com/en-us/windows/win32/com/appid-key [5] CLSID Key — Microsoft Learn — https://learn.microsoft.com/en-us/windows/win32/com/clsid-key-hklm [6] Registering COM Servers / InprocServer32 — Microsoft Learn — https://learn.microsoft.com/en-us/windows/win32/com/registering-com-servers [7] Distributed Component Object Model Protocol — DCOM/1.0 draft (Brown/Kindel; OBJREF/STDOBJREF/DUALSTRINGARRAY/MEOW, tower IDs) — https://download.samba.org/pub/samba/specs/draft-brown-dcom-v1-spec-03.txt (IETF mirror: https://datatracker.ietf.org/doc/html/draft-brown-dcom-v1-spec-03) [8] [MS-DCOM]: Distributed Component Object Model (DCOM) Remote Protocol — Microsoft Learn — https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/ [9] Understanding the DCOM Wire Protocol by Analyzing Network Data Packets — MSJ (March 1998, mirror) — https://jacobfilipp.com/MSJ/dcom.html [10] 3 Ways to Get a COM Server Process ID (OBJREF/STDOBJREF C structs, OXID/OID/IPID semantics) — Apriorit — https://www.apriorit.com/dev-blog/724-windows-three-ways-to-get-com-server-process-id [11] Inside COM+ (Dale Rogerson) — More on Monikers; DLL Surrogates; Marshaled Interface Pointers — https://thrysoee.dk/InsideCOM+/ch11c.htm ; https://thrysoee.dk/InsideCOM+/ch12b.htm ; https://thrysoee.dk/InsideCOM+/ch19e.htm [12] The Component Object Model (COM registry keys table, HKCR merged view, SCM lookup walkthrough) — magicsplat — https://www.magicsplat.com/book/com.html [13] COM Hijacking in Windows (HKCR/WOW6432Node, per-user vs system hives, InprocServer32/LocalServer32/TreatAs) — cyberc0re — https://www.cyberc0re.in/posts/com-hijacking-in-windows/ [14] Lateral Movement Using DCOM Objects and C# (CLSID/ProgID/AppID model, AppID requirement for DCOM) — klezvirus — https://klezvirus.github.io/RedTeaming/LateralMovement/LateralMovementDCOM/ [15] Yet another DCOM object for lateral movement (Control Panel abuse) — Kaspersky Securelist — https://securelist.com/lateral-movement-via-dcom-abusing-control-panel/118232/ [16] Abusing .NET Core CLR Diagnostic Features (+CVE-2023-33127; cross-session DCOM activation via "Interactive User" RunAs) — bohops (2023) — https://bohops.com/2023/11/27/abusing-net-core-clr-diagnostic-features-cve-2023-33127/ [17] OleViewDotNet — James Forshaw (tyranid) — https://github.com/tyranid/oleviewdotnet [18] DCOM Logical Privilege Escalation Part 6 (cites Forshaw "COM in Sixty Seconds"; ISystemActivator 000001A0, PropsOutInfo/MInterfacePointer→OBJREF) — https://ithelp.ithome.com.tw/articles/10339264 [19] CVE-2015-2370 DCOM DCE/RPC protocol analysis (IRemoteSCMActivator::RemoteCreateInstance replay LPE; ResolveOxid2) — https://security.zone.ci/AnQuanKe/AnQuanKeInfo/132266.html [20] KB5004442 — Manage changes for Windows DCOM Server Security Feature Bypass (CVE-2021-26414) — Microsoft Support — https://support.microsoft.com/en-au/topic/kb5004442-manage-changes-for-windows-dcom-server-security-feature-bypass-cve-2021-26414-f1400b52-c141-43d2-941e-37ed901c769c [21] DCOM authentication hardening: what you need to know — Microsoft Tech Community — https://techcommunity.microsoft.com/blog/windows-itpro-blog/dcom-authentication-hardening-what-you-need-to-know/3657154 [22] Microsoft DCOM Hardening Patch (CVE-2021-26414) — events 10036/10037/10038 details — SoftwareToolbox — https://help.softwaretoolbox.com/faq/microsoft-dcom-hardening [23] Attacking Visual Studio for Initial Access (LoadTypeLib moniker-parsing quirk; script moniker 06290BD3) — Outflank (2023) — https://www.outflank.nl/blog/2023/03/28/attacking-visual-studio-for-initial-access/ [24] Moniker Magic: Running Scripts Directly in Microsoft Office (script moniker CLSID, OleCreateLink bind chain) — leo00000 (PDF) — https://leo00000.github.io/pdf/Moniker_Magic_final.pdf [25] Office Moniker vulnerabilities overview (CVE-2017-8759, CVE-2018-8174, Office moniker blacklist CLSIDs) — https://zhuanlan.zhihu.com/p/352124245 [26] Automating COM/DCOM vulnerability research (OleViewDotNet-driven fuzzing; IDispatch JSON dump methodology) — incendium.rocks — https://incendium.rocks/posts/Automating-COM-Vulnerability-Research/ [27] Security Considerations for Requestors (CoInitializeSecurity hardening guidance) — Microsoft (MSDN archive) — https://msdn.microsoft.com/en-us/library/windows/desktop/aa384604(v=vs.85).aspx [28] CoInitializeSecurity (ole32) — EOAC/Authn/Imp enum values — pinvoke.net — http://pinvoke.net/default.aspx/ole32.CoInitializeSecurity [29] The case of COM failing to pump messages in a single-threaded COM apartment — Raymond Chen, The Old New Thing (2025) — https://devblogs.microsoft.com/oldnewthing/20250314-00/?p=110965 [30] Apartments and COM Threading Models (STA/MTA internals, hidden window, SCM activation modes) — Syracuse ECS CSE775 — https://ecs.syr.edu/faculty/fawcett/Handouts/cse775/Presentations/apartments.pdf [31] Windows Scheduled Tasks research (elevation-moniker requirements testing; Task Scheduler CLSID 0F87369F) — 3gstudent — https://3gstudent.github.io/backup-3gstudent.github.io/ [32] Reversing ICMLuaUtil: ShellExec and CallCustomActionDll for Elevated Execution — codedintrusion — https://codedintrusion.com/posts/reversing-icmluautil/ [33] Sharp4COMBypassUAC: ICMLuaUtil UAC bypass (elevation moniker + BIND_OPTS3 dwClassContext=4) — xz.aliyun.com — https://xz.aliyun.com/t/16063 [34] ALPHV ransomware analysis (CMSTPLUA elevation moniker in the wild; dllhost /Processid artifact) — d01a.github.io — https://d01a.github.io/alphv/ [35] Info Stealing Campaign Uses DLL Sideloading Through Cisco Webex Binaries (CoGetObject elevation moniker TTP, T1548.002) — Trellix — https://www.trellix.com/blogs/research/how-attackers-repackaged-a-threat-into-something-that-looked-benign/ [36] GhostWeaver PowerShell RAT (PEB masquerade + elevation moniker + ServerCreateElevatedObject) — derp.ca — https://www.derp.ca/research/ghostweaver-tag124-powershell-rat/ [37] In-Memory Loader Drops ScreenConnect (elevation moniker string obfuscation in loaders) — Zscaler ThreatLabz — https://www.zscaler.com/jp/blogs/security-research/memory-loader-drops-screenconnect [38] Lines Sample (canonical registry registration: ProgID/CurVer/LocalServer32/TypeLib/Interface ProxyStubClsid32=PSOAInterface) — Microsoft MSDN — https://msdn.microsoft.com/en-us/library/ms220980.aspx [39] MSI Packaging ebook — TypeLib table → HKCR\TypeLib{LIBID}<ver>\0\Win32, FLAGS, HELPDIR — Advanced Installer — https://www.advancedinstaller.com/application-packaging-training/msi/ebook/registries.html [40] Default CLSID/IID list Windows 10 19041 (scrobj.dll family: Scriptlet.Context/Constructor/Factory, Script moniker BD3, HostEncode BD4, TypeLib BD5) — inaz2 gist — https://gist.github.com/inaz2/5fa3ca02f4e8204a7dd4d4b50fd0c13f [41] STA vs MTA in COM: Apartments, Marshaling, and Deadlocks — comcomponent.com — https://comcomponent.com/en/blog/2026/01/31/000-sta-mta-com-relationship/ [42] COM in Sixty Seconds — James Forshaw (44CON 2017 talk; activation/OBJREF internals) — https://github.com/tyranid/presentations