mirror of
https://github.com/An0nUD4Y/Offensive-COM
synced 2026-07-20 09:09:57 +00:00
829 lines
77 KiB
Markdown
829 lines
77 KiB
Markdown
# 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](#1-why-com-matters-to-an-attacker)
|
||
- [2. The object model: interfaces are vtables](#2-the-object-model-interfaces-are-vtables)
|
||
- [3. Identity: the GUID zoo](#3-identity-the-guid-zoo)
|
||
- [4. The registry is the COM database](#4-the-registry-is-the-com-database)
|
||
- [5. Activation, step by step](#5-activation-step-by-step)
|
||
- [6. Marshaling and the wire](#6-marshaling-and-the-wire)
|
||
- [7. Apartments and threading](#7-apartments-and-threading)
|
||
- [8. Monikers and the Running Object Table](#8-monikers-and-the-running-object-table)
|
||
- [9. The COM security model](#9-the-com-security-model)
|
||
- [10. The elevation bridge](#10-the-elevation-bridge)
|
||
- [11. WinRT and .NET bridges](#11-winrt-and-net-bridges)
|
||
- [12. The attack-surface map](#12-the-attack-surface-map)
|
||
- [13. Tooling and lab setup](#13-tooling-and-lab-setup)
|
||
- [14. Research Lab: Building Your COM Research Workbench](#14-research-lab-building-your-com-research-workbench)
|
||
- [15. Quick-reference GUID appendix](#15-quick-reference-guid-appendix)
|
||
- [16. References](#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:
|
||
|
||
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:
|
||
|
||
```text
|
||
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}`:
|
||
|
||
```cpp
|
||
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 `vptr`s (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 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:
|
||
|
||
```cpp
|
||
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 `VARIANT`s — 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}`:
|
||
|
||
```cpp
|
||
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\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.
|
||
|
||

|
||
*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
|
||
|
||
```text
|
||
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_DESCRIPTOR`s, 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
|
||
|
||
```text
|
||
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]).
|
||
|
||
```text
|
||
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)
|
||
|
||
```cpp
|
||
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:
|
||
|
||
```cpp
|
||
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`)
|
||
|
||
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 `DllCanUnloadNow` → `S_OK` ⇒ `FreeLibrary`.
|
||
|
||
> **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`)
|
||
|
||

|
||
*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 `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 `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`)
|
||
|
||
```c
|
||
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
|
||
|
||
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`.
|
||
|
||

|
||
*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=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 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 `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** — `EnumRunning` 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 exposure** — `ROTFLAGS_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]):
|
||
|
||
```cpp
|
||
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 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]):
|
||
|
||
```text
|
||
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]).
|
||
|
||

|
||
*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 |
|
||
|
||

|
||
*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)*:
|
||
|
||
```powershell
|
||
# 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
|