77 KiB
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: 90–120 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
- 2. The object model: interfaces are vtables
- 3. Identity: the GUID zoo
- 4. The registry is the COM database
- 5. Activation, step by step
- 6. Marshaling and the wire
- 7. Apartments and threading
- 8. Monikers and the Running Object Table
- 9. The COM security model
- 10. The elevation bridge
- 11. WinRT and .NET bridges
- 12. The attack-surface map
- 13. Tooling and lab setup
- 14. Research Lab: Building Your COM Research Workbench
- 15. Quick-reference GUID appendix
- 16. References
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:
- 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]). - Activation is driven entirely by the registry. The mapping from a 128-bit class identifier to executable code lives in
HKEY_CLASSES_ROOT\CLSID, andHKEY_CLASSES_ROOTis a merged view in which per-user registrations silently override system ones (section 4.1). COM trusts that mapping; attackers edit it ([12], [13]). - 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.exewith/Processid:arguments,svchost -k DcomLaunchspawning aLocalServer32binary, 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 returnHRESULT; the first three slots are alwaysIUnknown.
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 orE_NOINTERFACE. The contract is reflexive, symmetric, transitive, andQI(IID_IUnknown)must always return the same pointer — that pointer is the object's identity.AddRef/Releaseimplement reference counting: the object frees itself when the count reaches zero.- Multiple interfaces on one object = multiple
vptrs (C++ multiple inheritance with adjusting thunks).IUnknownappears once per base vtable, but identity is preserved through the primaryIUnknown.
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 16–26 = facility (1 = RPC, 2 = Dispatch, 3 = Storage, 4 = ITF/interface-specific, 7 = Win32), bits 0–15 = 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 3–6:
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.
GetIDsOfNamestranslates names to numeric dispatch IDs (DISPIDs), andInvoke(DISPID, args)executes the method with arguments packed as an array ofVARIANTs — a tagged-union type that can hold integers, strings, objects, arrays. This is exactly whatNew-Object -ComObject, VBScriptCreateObject, JScript, VBA, and everyIDispatch-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-craftInvokecalls from a fuzzer or a C# reflection harness.- A dual interface exposes both a custom vtable and
IDispatch— markeddualin 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
Programmablemarker 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.Component → Vendor.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\CLSIDfirst (or highest in the merge order, section 4.1) decides whatWScript.Shellmeans 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.
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]):
TreatAs/AutoTreatAsare 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.- Then the SxS activation context (section 4.7).
- Then
InprocServer32(bitness-aware) orLocalServer32, depending on the requestedCLSCTX.
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 pureIDispatch.- Otherwise the CLSID points at a MIDL-generated proxy/stub DLL, which itself has a normal
HKCR\CLSIDentry withInprocServer32([38]).
Key idea: This key is the marshaling trust anchor. A per-user shadow of
HKCR\Interface\{IID}\ProxyStubClsid32injects 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:
LoadTypeLibmoniker-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/LegacyImpersonationLevelon 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
HKCRhunting, 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 onHKCU\Software\Classesand 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 |
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)
- combase resolves
HKCR\CLSID\{clsid}:TreatAs/AutoTreatAsfirst (recursive redirect), then the SxS activation context, thenInprocServer32in the bitness-appropriate view. LoadLibraryW(server.dll)— subject to DLL search order, KnownDLLs, and Safer/AppLocker policy.GetProcAddress("DllGetClassObject")→ callDllGetClassObject(rclsid, riid, &pv). Every COM DLL exportsDllGetClassObject,DllCanUnloadNow, and optionallyDllRegisterServer/DllUnregisterServer(invoked byregsvr32.exe).- The server returns its class factory;
IClassFactory::CreateInstancenews the object and QIs the requested IID. - Unload path:
CoFreeUnusedLibrariesperiodically callsDllCanUnloadNow→S_OK⇒FreeLibrary.
Key idea: Attack surface exists at every step: a TreatAs redirect (no DLL of the real class ever loads), a phantom
InprocServer32path 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)
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.
- combase sees no (or an excluded) in-proc server → asks the activator over ALPC/LRPC: on modern Windows this is the
ISystemActivator/IRemoteSCMActivatorinterface, GUID{000001A0-0000-0000-C000-000000000046}, implemented byrpcss.dlland mirrored in-process in combase for the local fast path ([18]). - RPCSS validates, in order:
HKLM\...\Ole!MachineLaunchRestriction→ AppIDLaunchPermission(orDefaultLaunchPermission) → identity selection:- default launching user (server started with the caller's token — integrity level inherited),
RunAs= specific user (prompted/stored credentials) orInteractive User(server in the currently logged-on session — the cross-session primitive, section 9.3),LocalService= start a service via the SCM.
- RPCSS (via DcomLaunch) starts the process: the
LocalServer32command line with the-Embeddingswitch, ordllhost.exe /Processid:{AppID}for surrogates, or the service. - The server process calls
CoInitializeEx, creates its class factories, thenCoRegisterClassObject(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, thenCoResumeClassObjects()atomically — avoids a race where a client activates a half-initialized server),SURROGATE=8. - RPCSS marshals the class-factory reference (an
OBJREF, section 6.2) back to the client. - Client unmarshals → proxy for
IClassFactory→ theCreateInstancecall goes over ORPC → server returns the requested interface pointer, again as anOBJREF; the client builds the interface proxy/stub chain fromHKCR\Interface\{IID}\ProxyStubClsid32. - Lifetime: the client holds refs; the server stays alive until the last external reference plus
LockServercount drop, then callsCoRevokeClassObjectand exits.
OpSec: A
LocalServer32spawn is noisy in a good way for defenders:svchost -k DcomLaunchis the parent, the child carries-Embeddingon 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 IActivationPropertiesOut → PropsOutInfo { 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
CoCreateInstanceExDCOM 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 (IRpcChannelBufferover the RPC runtime) → - Stub (server side,
IRpcStubBuffer::Invoke) unpacks, calls the real method, packs the return. - Factory:
IPSFactoryBuffer(GetProxyIID,CreateProxy,CreateStub), registered viaHKCR\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:
- 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. - Handler (
0x2): a light client-side handler DLL (InprocHandler32) plus a standard reference. - Custom (
0x4): the object implementsIMarshal({00000003-0000-0000-C000-000000000046}:GetUnmarshalClass,GetMarshalSizeMax,MarshalInterface,UnmarshalInterface,ReleaseMarshalData,DisconnectObject). AnOBJREF_CUSTOMcarries{CLSID of unmarshal class, cbExtension, size, pData[]}— an attacker-controlled blob deserialized byIMarshal::UnmarshalInterfaceof 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
CoGetInstanceFromIStorageOBJREF 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.
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
- 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]). - 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) andIRemUnknown2{00000143-...}handle remote QI and refcounting. - Pinging/liveness:
IOXIDResolver::SimplePing(pingId)andComplexPingkeep OIDs alive; missed pings ⇒ the server garbage-collects stubs; a clientReleasebecomesRemRelease, decrementingcPublicRefs.
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 — classOleMainThreadWndClass— whose WndProc dispatches cross-apartment calls ([30]). COINIT_MULTITHREADED=0x0→ MTA (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 returnsRPC_E_CHANGED_MODE.ThreadingModel=Neutralobjects 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]):
- 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. - 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.
- 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, orNeutralcomponents.
Key idea: Wrong
ThreadingModelnever 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 GetRunningObjectTable → IRunningObjectTable ({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/harvesting —
EnumRunningreveals 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 exposure —
ROTFLAGS_ALLOWANYCLIENTplus AppIDROTFlags=1makes 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?":IDENTIFYlets the server check who you are,IMPERSONATElets it act as you locally,DELEGATElets it act as you on other machines.
9.2 Where checks happen — the defense-in-depth chain
- Launch (RPCSS):
MachineLaunchRestriction→ AppIDLaunchPermission(elseDefaultLaunchPermission). Failure ⇒E_ACCESSDENIEDat activation and the classic "DistributedCOM 10016" log noise (event IDs 10015/10016 family). - Access (first call into the server): the server's
CoInitializeSecuritySD, else AppIDAccessPermission, elseDefaultAccessPermission, all gated byMachineAccessRestriction. - 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). - Impersonation (server side):
CoImpersonateClient()/CoRevertToSelf()— the client-supplieddwImpLevelcaps 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
IMPERSONATEorDELEGATE, you hold its token; cloaking bugs and wrong-identity activation are how "token kidnap" happens. Chain gates 1–3 decide whether you may talk; gate 4 decides whose token you walk away with.
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_DISABLEDpath guard); HKCR\CLSID\{CLSID}\Elevation\Enabled = 1(elseCO_E_ELEVATION_DISABLED);HKCR\CLSID\{CLSID}\LocalizedString = @dll,-resid— an MUI display name (elseCO_E_MISSING_DISPLAYNAME; MUI load errors fromRegLoadMUIStringWpropagate);- 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'shwndparents 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]).
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
IUnknownbase;IInspectable(IID{AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90};GetIids,GetRuntimeClassName,GetTrustLevel) replacesIDispatch. - Metadata lives in
.winmdfiles (ECMA-335 format) instead of TLBs. - Activation via
RoGetActivationFactory(HSTRING clsid, IID, void**)/RoActivateInstance. - Classes register per-package under
HKLM\SOFTWARE\Microsoft\WindowsRuntime(withActivatableClassIdserver mappings) — notHKCR\CLSID— so classic hijack tooling doesn't see them. Language projections (C++/WinRT, C#/WinRT,RoGetActivationFactoryshims) 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
IDispatchstubs at runtime to call arbitrary vtables from C# — the same trick the incendium.rocks fuzzing methodology uses to automateInvokeagainst 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 |
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), 10036–10038 (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, emptyDllSurrogate, 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 implementsIParseDisplayName/IMarshal(string/blob parsing).
14.4 Analysis workflow
- Baseline first. Snapshot the VM; export
HKCU\Software\ClassesandHKLM\SOFTWARE\Classes; record runningdllhostinstances and DistributedCOM event baselines. Every experiment gets a before/after diff. - Static: read the registration (section 4.2 checklist), the AppID, the interface's ProxyStubClsid32, the typelib; confirm threading model and elevation flags in OleViewDotNet.
- Dynamic: ProcMon with the section-13 filters during activation; WinDbg
bp combase!CoCreateInstanceandbp combase!CoGetObjectto capture exact strings and CLSCTX; Wireshark for any remote path (grepMEOW). - 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).
- Patch-diffing habit: when a COM CVE drops, diff
combase.dll/ole32.dll/rpcss.dllbetween 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