Call-Stack Laundering: Registration-Free COM as an Execution Primitive
Contents
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.
|
|
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.
The Contract: One Header, Two Independent Binaries
Before the loading variants, the architecture matters. The entire technique rests on a three-symbol interface definition:
|
|
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:
|
|
There is no CALL Det::Detonate instruction anywhere in the host binary. The
disassembly at the call site is:
|
|
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:
|
|
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:
|
|
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:
|
|
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):
|
|
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.
The exports required to satisfy DllGetClassObject:
|
|
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:
|
|
The manifest on disk maps the same CLSID to Payload.dll:
|
|
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:
|
|
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:
|
|
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:
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:
|
|
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.