The Problem

In 2019 I wrote about Registration-Free COM loading as a way for operators to avoid registry writes and sidestep the LoadLibrary + GetProcAddress combo that EDRs flag. The core technique holds up, but the detection story was incomplete in ways that matter operationally. This post rebuilds the topic from the ground up, corrects those gaps, and introduces three progressive loading variants that span the tradeoff space between simplicity and forensic stealth.


What Registration-Free COM Is

A COM server — whether a DLL or an EXE — is normally made discoverable by writing its CLSID into HKCR\CLSID\{...}\InprocServer32 via regsvr32. When a client calls CoGetClassObject, the COM runtime reads that key, finds the server binary, loads it, and returns the factory interface.

Registration-Free COM replaces the registry lookup with a Windows Activation Context (SxS manifest). The CLSID-to-file mapping lives in an XML manifest rather than the registry. The manifest can be a file on disk or embedded directly into the binary as a Win32 resource.

1
2
3
4
5
6
<assembly manifestVersion="1.0">
  <assemblyIdentity type="win32" name="Server" version="1.0.0.0" />
  <file name="Server.exe">
    <comClass clsid="{5d8a7d33-059f-418a-8d77-5f3944d63b6d}" threadingModel="Both" />
  </file>
</assembly>

No registry key is written. No elevation is required. The manifest above maps the CLSID to Server.exe — meaning the host can resolve a COM factory from itself.

flowchart LR subgraph trad["Traditional COM"] direction TB T1["Client\nCoGetClassObject"] --> T2["Registry\nHKCR\\CLSID\\{...}\\InprocServer32"] T2 --> T3["combase.dll\nLoadLibraryExW(server.dll)"] end subgraph regfree["Registration-Free COM"] direction TB R1["Client\nCoGetClassObject"] --> R2["SxS Manifest\nCLSID → file mapping"] R2 --> R3["combase.dll\nLoadLibraryExW(server.dll)"] end

The Contract: One Header, Two Independent Binaries

Before the loading variants, the architecture matters. The entire technique rests on a three-symbol interface definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// The CLSID carrier — __uuidof(Det) is what CoGetClassObject takes
struct __declspec(uuid("5d8a7d33-059f-418a-8d77-5f3944d63b6d")) Det;

// The payload interface — what the host calls after loading
struct __declspec(uuid("5a196c0f-e296-4b35-9249-f3d7ad5999fd")) IDet : IUnknown
{
    virtual void __stdcall Detonate()    = 0;
    virtual void __stdcall EndDetonate() = 0;
};

// The factory interface — what DllGetClassObject hands back
struct __declspec(uuid("9362f817-85b6-4a80-81de-772c792922ff")) IDetFactory : IUnknown
{
    virtual HRESULT __stdcall CreateDet(IDet** result) = 0;
};

This header is the only compile-time dependency between the host (WinMain) and any payload. The host never names the concrete implementation type. It never calls new Det. It holds only interface pointers and talks through vtables. This is not incidental — it is the property that breaks the static call graph.

The host side reduces to:

1
2
3
4
5
CoGetClassObject(__uuidof(Det), CLSCTX_INPROC_SERVER, nullptr,
                 __uuidof(armory), (void**)armory.GetAddressOf());

armory->CreateDet(det.GetAddressOf());
det->Detonate();

There is no CALL Det::Detonate instruction anywhere in the host binary. The disassembly at the call site is:

1
2
mov  rax, [rcx]        ; load vtable pointer from the IDet object
call [rax + 0x18]      ; jump through vtable slot — target is a runtime value

The payload’s address is resolved at runtime from a vtable filled in by the DLL instance. Static analysis cannot follow this.


Three Loading Variants

The same IDet contract works across three structurally distinct loading mechanisms. Each trades off stealth properties against operational prerequisites.

Case A — Self-Load via Embedded Manifest

The host binary embeds the manifest as RT_MANIFEST #1:

1
mt.exe -manifest Manifest.xml -outputresource:"Server.exe";#1

RT_MANIFEST #1 is the standard EXE manifest resource. Windows creates an activation context from it automatically at process start — no CreateActCtx call is needed. When CoGetClassObject runs, the activation context maps the CLSID to Server.exe, and combase.dll calls:

1
2
LoadLibraryExW("Server.exe")        ← second load of the same file
GetProcAddress(hMod, "DllGetClassObject")

Two things happen that matter for analysis. First, LoadLibraryExW loads the binary a second time, rebased by ASLR to a different address (e.g. EXE at 0x00400000, DLL copy at 0x6FD00000). The payload runs from the DLL copy’s address range. Any hook, tracer, or instrumentation anchored to the EXE’s address range misses the execution entirely. Second, the call stack at the moment LoadLibraryExW fires is:

1
2
3
4
KernelBase!LoadLibraryExW          ← the actual load
combase!CInprocServer::Load        ← called from a signed MS DLL
combase!CoGetClassObject           ← the only thing WinMain touched
Host!WinMain

LoadLibraryExW is not present in Server.exe’s import table. It is not called by any code in the binary. The EDR heuristic “untrusted binary called LoadLibrary” does not fire because the binary that called it is combase.dll.

This is call-stack laundering: the suspicious operation (LoadLibraryExW) exists and is visible, but its attributed caller is a signed Microsoft DLL.

The procmon stack at the Load Image event for Payload.dll confirms this. Frames are bottom-up (oldest first):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
44  Server.exe      WinMain + 0xd7   Host.cpp(23)        ← last host frame; CoGetClassObject call
43  combase.dll     CoGetClassObject + 0x4a              ← entry from host
42  combase.dll     CoGetClassObject + 0xcb4
41  combase.dll     RoGetActivatableClassRegistration
    ... (19 combase frames — COM's internal activation machinery) ...
30  combase.dll     CoGetClassObject + 0x19fc
29  combase.dll     RoGetActivationFactory
25  combase.dll     PropVariantClear + 0x2d3f            ← nearest export symbol; actual fn is CInprocServer::Load
24  KernelBase.dll  LoadLibraryExW + 0x156               ← the load; attributed to KernelBase, called by combase
 0  ntoskrnl.exe    NtMapViewOfSection                   ← kernel ImageLoad — fires regardless

Frame 44 is the deepest Server.exe frame. Everything below it belongs to signed Microsoft DLLs. LoadLibraryExW is called by combase.dll, not by the host binary. The frames above frame 24 (kernel, ntdll) confirm ETW ImageLoad still fires — the kernel is caller-agnostic.

sequenceDiagram box rgba(200,50,50,0.1) Direct Load — EDR sees host as caller participant HostD as loader.exe (WinMain) participant KBD as KernelBase.dll participant DLLD as payload.dll end box rgba(50,150,50,0.1) COM Load — EDR sees combase as caller participant Host as Server.exe (WinMain) participant COM as combase.dll participant KB as KernelBase.dll participant DLL as Payload.dll end HostD->>KBD: LoadLibraryExW("payload.dll") Note over HostD,KBD: ❌ host in call stack — EDR fires KBD->>DLLD: map into process HostD->>DLLD: GetProcAddress("RunPayload") DLLD-->>HostD: fn ptr HostD->>DLLD: call fn ptr Host->>COM: CoGetClassObject(CLSID) Note over COM: 19 combase frames COM->>KB: LoadLibraryExW("Payload.dll") Note over COM,KB: ✅ combase in call stack — host not attributed KB->>DLL: map into process Note over KB: kernel ETW ImageLoad fires regardless COM->>DLL: GetProcAddress("DllGetClassObject") DLL-->>COM: IDetFactory* COM-->>Host: IDetFactory* Host->>DLL: Detonate() via call [rax+0x18]

The exports required to satisfy DllGetClassObject:

1
2
3
4
EXPORTS
DllGetClassObject    PRIVATE
DllRegisterServer    PRIVATE
DllUnregisterServer  PRIVATE

PRIVATE keeps them out of the .lib import library while remaining reachable via GetProcAddress. DllRegisterServer and DllUnregisterServer are no-op stubs — regsvr32 can probe them without error, and no registry write occurs.

Case B — External DLL via Disk Manifest

The host accepts a manifest path at runtime and activates it around CoGetClassObject:

1
2
3
ActCtxRuntime actCtx(argv[2]);   // CreateActCtx + ActivateActCtx
CoGetClassObject(__uuidof(Det), CLSCTX_INPROC_SERVER, ...);
// ActCtxRuntime dtor: DeactivateActCtx + ReleaseActCtx

The manifest on disk maps the same CLSID to Payload.dll:

1
2
3
<file name="Payload.dll">
  <comClass clsid="{5d8a7d33-059f-418a-8d77-5f3944d63b6d}" threadingModel="Both" />
</file>

combase.dll resolves Payload.dll relative to the manifest’s directory and loads it. The call stack at load time is identical to Case A. The loaded DLL exports the same three symbols and implements the same IDet / IDetFactory interfaces.

The forensic difference from Case A:

Signal Case A Case B
<comClass> in RT_MANIFEST #1 Present — extractable statically Absent
DLL name anywhere in host binary Server.exe (self, anomalous) Nowhere
Self-loading anomaly Present Absent — normal COM client pattern

Case B’s call stack is indistinguishable from any legitimate application loading a COM add-in. Excel loading an in-process COM server produces the same shape.

The payload DLL defines no DllMain. When combase.dll loads it via LoadLibraryExW, the loader has no entry point to call — DLL_PROCESS_ATTACH never fires and LdrpCallInitRoutine is never invoked for this module. EDRs that instrument LdrpCallInitRoutine to intercept DLL initialization (a common userspace hook site for detecting injected or side-loaded code) receive no callback. The DLL is fully mapped and executable; the first user-visible execution from it is DllGetClassObject, called directly by combase.dll.

Case C — Embedded Manifest at Non-Standard Resource ID

Case C eliminates the on-disk manifest of Case B while keeping the external DLL. A second manifest is embedded in Server.exe at RT_MANIFEST #2:

1
mt.exe -manifest CaseC.manifest -outputresource:"Server.exe";#2

Resource #2 is not the standard EXE manifest. Windows does not process it automatically. Standard static analysis tools, PE analyzers, and sigcheck process RT_MANIFEST #1 only. #2 is invisible to them unless the analyst specifically enumerates all resource entries.

At runtime, the host extracts resource #2, writes it briefly to the DLL directory as a temp file, calls CreateActCtx against that file (which parses the manifest into memory structures), then immediately deletes the temp file before CoGetClassObject runs:

1
2
3
4
Process start  →  no manifest file on disk
CreateActCtx   →  ~actctx.manifest written briefly to DLL directory
CreateActCtx returns  →  ~actctx.manifest deleted
CoGetClassObject  →  no manifest file on disk anywhere

The activation context is parsed into memory at CreateActCtx time. The file is not needed again. No artifact remains on disk when the load occurs.

The directory supplied at runtime serves two purposes simultaneously: it becomes the assembly root for <file> resolution (so combase.dll constructs directory\Payload.dll as the full path), and it is where the temp manifest must be written. These conditions cannot be separated.

Signal Case A Case B Case C
<comClass> visible to standard tools Yes (#1) N/A (file) No (#2 only)
Manifest file on disk during load None Required None
DLL name in host binary Server.exe None In #2 resource — non-standard location
Self-load anomaly Yes No No
DLL directory in host binary N/A None None — runtime input only

How each case sources its activation context before CoGetClassObject runs:

flowchart TD START([Server.exe starts]) --> ARG{switch} ARG -->|Case A: /A| CA_CTX[RT_MANIFEST #1 auto-activated at process start] ARG -->|Case B: /B path| CB_CTX[CreateActCtx from disk manifest file] ARG -->|Case C: /C dir| CC_RES[FindResource: extract RT_MANIFEST #2 from binary] CC_RES --> CC_WRITE[WriteFile: dir/~actctx.manifest written briefly] CC_WRITE --> CC_ACT[CreateActCtx: manifest parsed into memory] CC_ACT --> CC_DEL[DeleteFile: no manifest on disk] CA_CTX --> CGCO[CoGetClassObject] CB_CTX --> CGCO CC_DEL --> CGCO CGCO --> LOAD[combase.dll calls LoadLibraryExW + DllGetClassObject] LOAD --> IFACE[IDetFactory → IDet → Detonate via vtable]

What This Technique Actually Bypasses

The 2019 post framed the value as “avoiding the LoadLibrary + GetProcAddress combo.” That framing is imprecise in a way that matters. LoadLibraryExW does execute — it is called by combase.dll. GetProcAddress also executes — combase.dll calls it to locate DllGetClassObject. Neither appears in the host binary’s import table or source code because the host binary never calls them. They are called by a signed Microsoft DLL on behalf of the host.

What the technique actually bypasses:

Detection Method Bypassed Mechanism
IAT scan for LoadLibrary* Yes Not in host’s import table
Dynamic GetProcAddress("LoadLibrary") pattern Yes Never called by host code
PEB walk / hash-based API resolution Yes Never done
“Untrusted module called LoadLibrary” heuristic Yes Caller is combase.dll — signed, trusted
Userspace API hook on LoadLibraryA/W in host module Yes Hook on host’s address range is never reached
EDR hook on DllMain / LdrpCallInitRoutine Yes Payload DLL has no DllMain; DLL_PROCESS_ATTACH never fires; LdrpCallInitRoutine is never invoked for this load
Registry CLSID write (HKCR\CLSID) monitoring Yes Activation context replaces registry entirely
Two-file artifact pattern (loader.exe + payload.dll) Yes (Case A) Single binary — no second artifact
Static call graph from WinMain to payload Yes No direct call edge — vtable dispatch breaks the graph

What This Technique Does Not Bypass

This is the part the 2019 post underweighted.

Detection Method Still Fires Why
Kernel PsSetLoadImageNotifyRoutine Yes Fires regardless of which module called LoadLibrary
ETW ImageLoad (Microsoft-Windows-Kernel-Process) Yes Kernel-generated, caller-agnostic
Loaded module characteristics (unsigned, unusual path) Yes The DLL’s metadata is still inspectable
RT_MANIFEST #1 anomaly analysis (Case A) Yes EXE registering itself as a COM server is forensically unusual
Payload behavior (CreateFile, WriteFile, network, etc.) Yes File system minifilter and network driver see I/O regardless of execution path
Static binary analysis (YARA, strings, disassembly) Yes The payload code is on disk
Memory forensics (pe-sieve, volatility) Yes In-memory PE is inspectable

The technique’s ceiling is userspace and signature-based detection — the layer that catches commodity malware and drives a large portion of alert volume for older-generation products. It is not a bypass for kernel-driver EDRs with ImageLoad callbacks, behavioral analytics that model self-loading as anomalous, or a thorough human analyst.

The operational value is not invisibility. It is cost elevation: making automated first-pass analysis return nothing interesting, and requiring a detection chain that operates at or below the kernel to reconstruct the execution path. The binary walks into a sandbox, loads its payload through CoGetClassObject, and the sandbox reports “normal COM usage.” A human analyst following the call graph from WinMain hits CoGetClassObject and has to know to look for the activation context, enumerate RT_MANIFEST #2, and correlate the temp manifest write with the subsequent load event to reconstruct what happened. That is meaningfully more work than following a direct LoadLibrary call.


The Invocation Surface

All three cases use the same binary and the same interface. The switch is a command-line argument:

1
2
3
Server.exe /A                                      # Case A: self-load, no prerequisites
Server.exe /B C:\Code\Armory\Payload\Dist\Payload.manifest  # Case B: external DLL, disk manifest
Server.exe /C C:\Code\Armory\Payload\Dist\         # Case C: external DLL, embedded manifest #2

The host binary (Server.exe) is a reusable dispatch shell. The payload (Payload.dll) is independently deployable and can be swapped without recompiling the host. Both compile against the same three-symbol Interfaces.h contract and nothing else.


MITRE ATT&CK

Technique ID
System Binary Proxy Execution T1218
Component Object Model T1559.001
Reflective Code Loading T1620
Indirect Command Execution T1202
Modify Registry (avoided) T1112

This technique does not map cleanly to any single ATT&CK entry — it is the combination that matters. T1559.001 (COM) is the mechanism; T1218 (System Binary Proxy Execution) describes the attribution effect: a signed OS component (combase.dll) performs the load on behalf of the host. T1620 (Reflective Code Loading) captures the vtable-mediated execution where no direct call edge from WinMain to payload code exists in the binary. T1202 (Indirect Command Execution) reflects the broader principle that the host binary’s observable behavior is decoupled from the payload’s actual execution. Defenders correlating on any single technique ID in isolation will miss the chain.


Prior Art

Registration-Free COM as a loading primitive has been discovered and rediscovered independently across several years.

2019 — original writeup. The technique was first documented here in the context of avoiding registry writes and the LoadLibrary + GetProcAddress call pattern that first-generation EDRs flagged. That post covered what is now called Case A — the self-loading EXE via RT_MANIFEST #1. The detection analysis in that post was incomplete: it understated what kernel-level telemetry still fires and framed the value as “avoiding LoadLibrary” rather than the more precise “laundering the LoadLibrary call through a signed Microsoft DLL.” This post corrects those gaps and adds Cases B and C as two further primitives along the tradeoff curve.

2019 — Philip Tsukerman, “Activation Contexts: A Love Story”. Tsukerman documented activation context abuse for a different goal: poisoning existing application activation contexts to achieve persistence by redirecting COM server resolution without registry writes. The mechanism (CreateActCtx / activation context override) overlaps with what is described here; the intent differs — persistence hijack versus clean-stack loading. The parallel independent discovery confirms that activation contexts were an underexplored primitive at the time.

2023 — 0xDarkVortex, thread-pool COM proxying. A post-2019 refinement targeting the same call-stack attribution problem via a different route: routing LoadLibrary through Windows thread-pool APIs (TpAllocWork / TpPostWork) so that the attributed caller is ntdll!TppWorkerThread rather than host code. Same goal — make the suspicious load appear to originate from a trusted system component — achieved without COM. The convergence on this class of problem from multiple independent directions indicates it represents a genuine detection gap in userspace-anchored EDR architectures.

COM hijacking (general). A large body of work exists on abusing registered COM servers for persistence and lateral movement (e.g., HKCU\Software\Classes\CLSID hijacks). That class of technique requires registry writes and targets existing CLSIDs. Registration-Free COM is structurally distinct: no registry write occurs, no existing registration is hijacked, and the CLSID is under operator control.


Summary

Registration-Free COM gives red teams a native Windows loading primitive that routes LoadLibraryExW through a signed Microsoft DLL, severs the static call graph from entry point to payload, and requires no registry writes, no second file (Case A), and no elevated privileges. The three loading variants trade static indicators against operational prerequisites: Case A is the simplest and most self-contained; Case C leaves the fewest forensic artifacts at the cost of requiring a writable directory and an external DLL.

The technique does not defeat kernel-level telemetry or behavioral analytics. It defeats the tooling that looks for suspicious strings in import tables, direct LoadLibrary calls from untrusted modules, and linear call graphs from process entry point to suspicious API. On that layer, a process using this pattern looks identical to any standard COM client application — which is precisely the point.